Merge branch 'cleanuphelper' of https://github.com/argonui/SCED into cleanuphelper

This commit is contained in:
Chr1Z93 2022-12-16 11:53:23 +01:00
commit 94c5cd87af
56 changed files with 822 additions and 1115 deletions

View File

@ -67,6 +67,10 @@
"ScriptingTrigger.67ce9a",
"Detailedphasereference.68fe54",
"RulesIndex.91c83e",
"Clues.3f22e5",
"Clues.db85d6",
"Clues.891403",
"Clues.4111de",
"Resources.4406f0",
"Damage.eb08d6",
"Horror.468e88",
@ -129,11 +133,10 @@
"LeakedItems.42cd6e",
"ChaosTokenReserve.106418",
"3DText.134348",
"Custom_Model.032300",
"Custom_Model.1769ed",
"Custom_Model.37be78",
"Custom_Model.d86b7c",
"ClueCounterSwapper.d919d6",
"ClueCounter.37be78",
"ClueCounter.1769ed",
"ClueCounter.032300",
"ClueCounter.d86b7c",
"MasterClueCounter.4a3aa4",
"LegacyAssets.7165a9",
"Weaknessdecks.750fdd",
@ -265,5 +268,5 @@
"Tags": [],
"Turns_path": "Turns.json",
"VersionNumber": "v13.2.2",
"XmlUI": "<Include src=\"Global.xml\"/>"
"XmlUI": "\u003cInclude src=\"Global.xml\"/\u003e"
}

View File

@ -46,16 +46,16 @@
"LuaScriptState": "",
"MeasureMovement": false,
"Name": "Custom_Model",
"Nickname": "",
"Nickname": "Clue Counter",
"Snap": true,
"Sticky": true,
"Tooltip": true,
"Tooltip": false,
"Transform": {
"posX": -31.911,
"posY": 1.57,
"posZ": 30.97,
"posX": -32.193,
"posY": 1.52,
"posZ": 30.977,
"rotX": 0,
"rotY": 0,
"rotY": 10,
"rotZ": 0,
"scaleX": 0.33,
"scaleY": 0.33,

View File

@ -46,16 +46,16 @@
"LuaScriptState": "",
"MeasureMovement": false,
"Name": "Custom_Model",
"Nickname": "",
"Nickname": "Clue Counter",
"Snap": true,
"Sticky": true,
"Tooltip": true,
"Tooltip": false,
"Transform": {
"posX": -59.449,
"posY": 1.57,
"posZ": -22.628,
"posX": -59.426,
"posY": 1.52,
"posZ": -22.721,
"rotX": 0,
"rotY": 270,
"rotY": 280,
"rotZ": 0,
"scaleX": 0.33,
"scaleY": 0.33,

View File

@ -46,16 +46,16 @@
"LuaScriptState": "",
"MeasureMovement": false,
"Name": "Custom_Model",
"Nickname": "",
"Nickname": "Clue Counter",
"Snap": true,
"Sticky": true,
"Tooltip": true,
"Tooltip": false,
"Transform": {
"posX": -18.983,
"posY": 1.57,
"posZ": -31.01,
"posX": -18.87,
"posY": 1.52,
"posZ": -30.977,
"rotX": 0,
"rotY": 180,
"rotY": 190,
"rotZ": 0,
"scaleX": 0.33,
"scaleY": 0.33,

View File

@ -46,16 +46,16 @@
"LuaScriptState": "",
"MeasureMovement": false,
"Name": "Custom_Model",
"Nickname": "",
"Nickname": "Clue Counter",
"Snap": true,
"Sticky": true,
"Tooltip": true,
"Tooltip": false,
"Transform": {
"posX": -59.499,
"posY": 1.57,
"posZ": 9.561,
"posX": -59.426,
"posY": 1.52,
"posZ": 9.395,
"rotX": 0,
"rotY": 270,
"rotY": 280,
"rotZ": 0,
"scaleX": 0.33,
"scaleY": 0.33,

View File

@ -1,77 +0,0 @@
{
"AltLookAngle": {
"x": 0,
"y": 0,
"z": 0
},
"Autoraise": true,
"Bag": {
"Order": 0
},
"ColorDiffuse": {
"b": 1,
"g": 0.99217,
"r": 1
},
"ContainedObjects_order": [
"Clues.3f22e5",
"Clues.4111de",
"Clues.891403",
"Clues.db85d6"
],
"ContainedObjects_path": "ClueCounterSwapper.d919d6",
"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/1179328606460871995/F2AFA106E788BB456C6F9134CE7A7B14D510F973/",
"MaterialIndex": 3,
"MeshURL": "http://pastebin.com/raw.php?i=uWAmuNZ2",
"NormalURL": "",
"TypeIndex": 6
},
"Description": "Counter mode courtesy of tadgh's clue counter mod: https://steamcommunity.com/sharedfiles/filedetails/?id=2115363630",
"DragSelectable": true,
"GMNotes": "",
"GUID": "d919d6",
"Grid": true,
"GridProjection": false,
"Hands": false,
"HideWhenFaceDown": false,
"IgnoreFoW": false,
"LayoutGroupSortIndex": 0,
"Locked": true,
"LuaScript": "require(\"util/ClueCounterSwapper\")",
"LuaScriptState_path": "ClueCounterSwapper.d919d6.luascriptstate",
"MaterialIndex": -1,
"MeasureMovement": false,
"MeshIndex": -1,
"Name": "Custom_Model_Bag",
"Nickname": "Clue Counter Swapper",
"Snap": true,
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -50.9,
"posY": 1.51,
"posZ": 0,
"rotX": 0,
"rotY": 270,
"rotZ": 0,
"scaleX": 0.4,
"scaleY": 0.01,
"scaleZ": 0.4
},
"Value": 0,
"XmlUI": ""
}

View File

@ -1 +0,0 @@
{"ml":{"3f22e5":{"lock":true,"pos":{"x":-59.50,"y":1.54,"z":9.56},"rot":{"x":0,"y":280,"z":0}},"4111de":{"lock":true,"pos":{"x":-59.45,"y":1.54,"z":-22.63},"rot":{"x":0,"y":280,"z":0}},"891403":{"lock":true,"pos":{"x":-31.91,"y":1.54,"z":30.97},"rot":{"x":0,"y":10,"z":0}},"db85d6":{"lock":true,"pos":{"x":-18.98,"y":1.54,"z":-31.01},"rot":{"x":0,"y":190,"z":0}}}}

View File

@ -40,13 +40,16 @@
"Nickname": "Clues",
"Snap": true,
"Sticky": true,
"Tags": [
"CleanUpHelper_ignore"
],
"Tooltip": false,
"Transform": {
"posX": -59.318,
"posY": 1.64,
"posZ": -17.674,
"posX": -59.426,
"posY": 1.3,
"posZ": -22.721,
"rotX": 0,
"rotY": 270,
"rotY": 280,
"rotZ": 0,
"scaleX": 0.26,
"scaleY": 1,

View File

@ -40,13 +40,16 @@
"Nickname": "Clues",
"Snap": true,
"Sticky": true,
"Tags": [
"CleanUpHelper_ignore"
],
"Tooltip": false,
"Transform": {
"posX": -23.81,
"posY": 1.589,
"posZ": -30.927,
"posX": -18.87,
"posY": 1.3,
"posZ": -30.977,
"rotX": 0,
"rotY": 180,
"rotY": 190,
"rotZ": 0,
"scaleX": 0.26,
"scaleY": 1,

View File

@ -40,13 +40,16 @@
"Nickname": "Clues",
"Snap": true,
"Sticky": true,
"Tags": [
"CleanUpHelper_ignore"
],
"Tooltip": false,
"Transform": {
"posX": -31.911,
"posY": 1.564,
"posZ": 30.92,
"posX": -32.193,
"posY": 1.3,
"posZ": 30.977,
"rotX": 0,
"rotY": 0,
"rotY": 10,
"rotZ": 0,
"scaleX": 0.26,
"scaleY": 1,

View File

@ -40,13 +40,16 @@
"Nickname": "Clues",
"Snap": true,
"Sticky": true,
"Tags": [
"CleanUpHelper_ignore"
],
"Tooltip": false,
"Transform": {
"posX": -59.439,
"posY": 1.637,
"posZ": 9.472,
"posX": -59.426,
"posY": 1.3,
"posZ": 9.395,
"rotX": 0,
"rotY": 270,
"rotY": 280,
"rotZ": 0,
"scaleX": 0.26,
"scaleY": 1,

View File

@ -70,7 +70,7 @@
"Transform": {
"posX": -1.309,
"posY": 1.483,
"posZ": 0.034,
"posZ": 0,
"rotX": 0,
"rotY": 270,
"rotZ": 0,

View File

@ -46,9 +46,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -32.588,
"posX": -32.6,
"posY": 1.531,
"posZ": 19.301,
"posZ": 19.35,
"rotX": 0,
"rotY": 0,
"rotZ": 0,

View File

@ -48,7 +48,7 @@
"Transform": {
"posX": -47.76,
"posY": 1.531,
"posZ": -23.116,
"posZ": -23.1,
"rotX": 0,
"rotY": 270,
"rotZ": 0,

View File

@ -48,7 +48,7 @@
"Transform": {
"posX": -47.75,
"posY": 1.531,
"posZ": 9,
"posZ": 9.1,
"rotX": 0,
"rotY": 270,
"rotZ": 0,

View File

@ -46,9 +46,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -18.475,
"posX": -18.6,
"posY": 1.531,
"posZ": -19.301,
"posZ": -19.35,
"rotX": 0,
"rotY": 180,
"rotZ": 0,

View File

@ -1157,9 +1157,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -19.06,
"posX": -19.17,
"posY": 1.55,
"posZ": -24.78,
"posZ": -24.845,
"rotX": 0,
"rotY": 180,
"rotZ": 0,

View File

@ -1157,9 +1157,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -53.206,
"posX": -53.219,
"posY": 1.55,
"posZ": 8.432,
"posZ": 8.513,
"rotX": 0,
"rotY": 270,
"rotZ": 0,

View File

@ -1157,9 +1157,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -53.206,
"posX": -53.219,
"posY": 1.55,
"posZ": 9.573,
"posZ": 9.657,
"rotX": 0,
"rotY": 270,
"rotZ": 0,

View File

@ -1157,9 +1157,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -34.264,
"posX": -34.293,
"posY": 1.55,
"posZ": 24.8,
"posZ": 24.864,
"rotX": 0,
"rotY": 0,
"rotZ": 0,

View File

@ -1157,9 +1157,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -53.265,
"posX": -53.269,
"posY": 1.55,
"posZ": -22.542,
"posZ": -22.541,
"rotX": 0,
"rotY": 270,
"rotZ": 0,

View File

@ -1157,9 +1157,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -53.265,
"posX": -53.269,
"posY": 1.55,
"posZ": -24.825,
"posZ": -24.824,
"rotX": 0,
"rotY": 270,
"rotZ": 0,

View File

@ -1157,9 +1157,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -17.918,
"posX": -18.025,
"posY": 1.55,
"posZ": -24.783,
"posZ": -24.845,
"rotX": 0,
"rotY": 180,
"rotZ": 0,

View File

@ -1157,9 +1157,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -31.981,
"posX": -32.011,
"posY": 1.55,
"posZ": 24.8,
"posZ": 24.864,
"rotX": 0,
"rotY": 0,
"rotZ": 0,

View File

@ -1157,9 +1157,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -16.777,
"posX": -16.887,
"posY": 1.55,
"posZ": -24.783,
"posZ": -24.845,
"rotX": 0,
"rotY": 180,
"rotZ": 0,

View File

@ -1157,9 +1157,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -33.123,
"posX": -33.149,
"posY": 1.55,
"posZ": 24.8,
"posZ": 24.864,
"rotX": 0,
"rotY": 0,
"rotZ": 0,

View File

@ -1157,9 +1157,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -53.206,
"posX": -53.219,
"posY": 1.55,
"posZ": 7.29,
"posZ": 7.374,
"rotX": 0,
"rotY": 270,
"rotZ": 0,

View File

@ -1157,9 +1157,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -53.265,
"posX": -53.269,
"posY": 1.55,
"posZ": -23.683,
"posZ": -23.679,
"rotX": 0,
"rotY": 270,
"rotZ": 0,

View File

@ -34,7 +34,7 @@
"LayoutGroupSortIndex": 0,
"Locked": true,
"LuaScript": "require(\"core/MasterClueCounter\")",
"LuaScriptState": "",
"LuaScriptState": "false",
"MeasureMovement": false,
"Name": "Custom_Token",
"Nickname": "Master Clue Counter\n",

View File

@ -282,9 +282,9 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -54.989,
"posX": -55,
"posY": 1.45,
"posZ": 16.018,
"posZ": 16.1,
"rotX": 0,
"rotY": 270,
"rotZ": 0,

View File

@ -11,5 +11,7 @@ DISCARD_PILE_POSITION = { x = -58.9, y = 4, z = 4.29 }
TRASHCAN_GUID = "147e80"
STAT_TRACKER_GUID = "e598c2"
RESOURCE_COUNTER_GUID = "4406f0"
CLUE_COUNTER_GUID = "d86b7c"
CLUE_CLICKER_GUID = "db85d6"
require("playermat/Playmat")

View File

@ -11,5 +11,7 @@ DISCARD_PILE_POSITION = { x = -58.96, y = 4, z = -27.82 }
TRASHCAN_GUID = "f7b6c8"
STAT_TRACKER_GUID = "b4a5f7"
RESOURCE_COUNTER_GUID = "816d84"
CLUE_COUNTER_GUID = "1769ed"
CLUE_CLICKER_GUID = "3f22e5"
require("playermat/Playmat")

View File

@ -282,9 +282,9 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -25.57,
"posX": -25.6,
"posY": 1.45,
"posZ": 26.54,
"posZ": 26.6,
"rotX": 0,
"rotY": 0,
"rotZ": 0,

View File

@ -11,5 +11,7 @@ DISCARD_PILE_POSITION = { x = -37.26, y = 4, z = 30.50 }
TRASHCAN_GUID = "5f896a"
STAT_TRACKER_GUID = "af7ed7"
RESOURCE_COUNTER_GUID = "cd15ac"
CLUE_COUNTER_GUID = "032300"
CLUE_CLICKER_GUID = "891403"
require("playermat/Playmat")

View File

@ -282,9 +282,9 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -25.493,
"posX": -25.6,
"posY": 1.45,
"posZ": -26.54,
"posZ": -26.6,
"rotX": 0,
"rotY": 180,
"rotZ": 0,

View File

@ -11,5 +11,7 @@ DISCARD_PILE_POSITION = { x = -13.78, y = 4, z = -30.48 }
TRASHCAN_GUID = "4b8594"
STAT_TRACKER_GUID = "e74881"
RESOURCE_COUNTER_GUID = "a4b60d"
CLUE_COUNTER_GUID = "37be78"
CLUE_CLICKER_GUID = "4111de"
require("playermat/Playmat")

View File

@ -31,9 +31,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -13.738,
"posX": -14.05,
"posY": 1.481,
"posZ": -28.511,
"posZ": -28.6,
"rotX": 0,
"rotY": 180,
"rotZ": 0,

View File

@ -31,7 +31,7 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -56.926,
"posX": -57,
"posY": 1.544,
"posZ": 4.545,
"rotX": 0,

View File

@ -31,9 +31,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -56.928,
"posX": -57,
"posY": 1.539,
"posZ": -27.729,
"posZ": -27.65,
"rotX": 0,
"rotY": 270,
"rotZ": 0,

View File

@ -31,9 +31,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -36.964,
"posX": -37.15,
"posY": 1.468,
"posZ": 28.475,
"posZ": 28.6,
"rotX": 0,
"rotY": 0,
"rotZ": 0,

View File

@ -48,7 +48,7 @@
"Transform": {
"posX": 0.493,
"posY": 1.656,
"posZ": 0.023,
"posZ": 0,
"rotX": 0,
"rotY": 0,
"rotZ": 0,

View File

@ -0,0 +1,439 @@
do
local playAreaApi = require("core/PlayAreaApi")
local ArkhamDb = { }
local internal = { }
local RANDOM_WEAKNESS_ID = "01000"
local tabooList = { }
--Forward declaration
---@type Request
local Request = {}
local configuration
-- Sets up the ArkhamDb interface. Should be called from the parent object on load.
ArkhamDb.initialize = function()
configuration = internal.getConfiguration()
Request.start({ configuration.api_uri, configuration.taboo }, function(status)
local json = JSON.decode(internal.fixUtf16String(status.text))
for _, taboo in pairs(json) do
---@type <string, boolean>
local cards = {}
for _, card in pairs(JSON.decode(taboo.cards)) do
cards[card.code] = true
end
tabooList[taboo.id] = {
date = taboo.date_start,
cards = cards
}
end
return true, nil
end)
end
-- Start the deck build process for the given player color and deck ID. This
-- will retrieve the deck from ArkhamDB, and pass to a callback for processing.
---@param playerColor String. Color name of the player mat to place this deck on (e.g. "Red").
---@param deckId String. ArkhamDB deck id to be loaded
---@param isPrivate Boolean. Whether this deck is published or private on ArkhamDB
---@param loadNewest Boolean. Whether the newest version of this deck should be loaded
---@param loadInvestigators Boolean. Whether investigator cards should be loaded as part of this
--- deck
---@param callback Function. Callback which will be sent the results of this load. Parameters
--- to the callback will be:
--- slots Table. A map of card ID to count in the deck
--- investigatorCode String. ID of the investigator in this deck
--- customizations Table. The decoded table of customization upgrades in this deck
--- playerColor String. Color this deck is being loaded for
ArkhamDb.getDecklist = function(
playerColor,
deckId,
isPrivate,
loadNewest,
loadInvestigators,
callback)
-- Get a simple card to see if the bag indexes are complete. If not, abort
-- the deck load. The called method will handle player notification.
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
local checkCard = allCardsBag.call("getCardById", { id = "01001" })
if (checkCard ~= nil and checkCard.data == nil) then
return
end
local deckUri = { configuration.api_uri,
isPrivate and configuration.private_deck or configuration.public_deck, deckId }
local deck = Request.start(deckUri, function(status)
if string.find(status.text, "<!DOCTYPE html>") then
printToAll("Private deck ID " .. deckId .. " is not shared", playerColor)
return false, table.concat({ "Private deck ", deckId, " is not shared" })
end
local json = JSON.decode(status.text)
if not json then
printToAll("Deck ID " .. deckId .. " not found", playerColor)
return false, "Deck not found!"
end
return true, JSON.decode(status.text)
end)
deck:with(internal.onDeckResult, playerColor, loadNewest, loadInvestigators, callback)
end
-- Logs that a card could not be loaded in the mod by printing it to the console in the given
-- color of the player owning the deck. Attempts to look up the name on ArkhamDB for clarity,
-- but prints the card ID if the name cannot be retrieved.
---@param cardId String. ArkhamDB ID of the card that could not be found
---@param playerColor String. Color of the player's deck that had the problem
ArkhamDb.logCardNotFound = function(cardId, playerColor)
local request = Request.start({
configuration.api_uri,
configuration.cards,
cardId
},
function(result)
local adbCardInfo = JSON.decode(internal.fixUtf16String(result.text))
local cardName = adbCardInfo.real_name
if (cardName ~= nil) then
if (adbCardInfo.xp ~= nil and adbCardInfo.xp > 0) then
cardName = cardName .. " (" .. adbCardInfo.xp .. ")"
end
printToAll("Card not found: " .. cardName .. ", ArkhamDB ID " .. cardId, playerColor)
else
printToAll("Card not found in ArkhamDB, ID " .. cardId, playerColor)
end
end)
end
-- Callback when the deck information is received from ArkhamDB. Parses the
-- response then applies standard transformations to the deck such as adding
-- random weaknesses and checking for taboos. Once the deck is processed,
-- passes to loadCards to actually spawn the defined deck.
---@param deck ArkhamImportDeck
---@param playerColor String Color name of the player mat to place this deck on (e.g. "Red")
---@param loadNewest Boolean. Whether the newest version of this deck should be loaded
---@param loadInvestigators Boolean. Whether investigator cards should be loaded as part of this
--- deck
---@param callback Function. Callback which will be sent the results of this load. Parameters
--- to the callback will be:
--- slots Table. A map of card ID to count in the deck
--- investigatorCode String. ID of the investigator in this deck
--- bondedList A table of cardID keys to meaningless values. Card IDs in this list were
--- added from a parent bonded card.
--- customizations Table. The decoded table of customization upgrades in this deck
--- playerColor String. Color this deck is being loaded for
internal.onDeckResult = function(deck, playerColor, loadNewest, loadInvestigators, callback)
-- Load the next deck in the upgrade path if the option is enabled
if (loadNewest and deck.next_deck ~= nil and deck.next_deck ~= "") then
buildDeck(playerColor, deck.next_deck)
return
end
printToAll(table.concat({ "Found decklist: ", deck.name }), playerColor)
log(table.concat({ "-", deck.name, "-" }))
for k, v in pairs(deck) do
if type(v) == "table" then
log(table.concat { k, ": <table>" })
else
log(table.concat { k, ": ", tostring(v) })
end
end
-- Initialize deck slot table and perform common transformations. The order of these should not
-- be changed, as later steps may act on cards added in each. For example, a random weakness or
-- investigator may have bonded cards or taboo entries, and should be present
local slots = deck.slots
internal.maybeDrawRandomWeakness(slots, playerColor)
if loadInvestigators then
internal.addInvestigatorCards(deck, slots)
end
internal.maybeAddCustomizeUpgradeSheets(slots)
internal.maybeAddSummonedServitor(slots)
internal.maybeAddOnTheMend(slots, playerColor)
local bondList = internal.extractBondedCards(slots)
internal.checkTaboos(deck.taboo_id, slots, playerColor)
-- get upgrades for customizable cards
local meta = deck.meta
local customizations = {}
if meta then customizations = JSON.decode(deck.meta) end
callback(slots, deck.investigator_code, bondList, customizations, playerColor)
end
-- Checks to see if the slot list includes the random weakness ID. If it does,
-- removes it from the deck and replaces it with the ID of a random basic weakness provided by the
-- all cards bag
---@param slots The slot list for cards in this deck. Table key is the cardId, value is the number
--- of those cards which will be spawned
---@param playerColor Color name of the player this deck is being loaded for. Used for broadcast
--- if a weakness is added.
internal.maybeDrawRandomWeakness = function(slots, playerColor)
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
local hasRandomWeakness = false
for cardId, cardCount in pairs(slots) do
if cardId == RANDOM_WEAKNESS_ID then
hasRandomWeakness = true
break
end
end
if hasRandomWeakness then
local weaknessId = allCardsBag.call("getRandomWeaknessId")
slots[weaknessId] = 1
slots[RANDOM_WEAKNESS_ID] = nil
printToAll("Random basic weakness added to deck", playerColor)
end
end
-- Adds both the investigator (XXXXX) and minicard (XXXXX-m) slots with one copy each
---@param deck The processed ArkhamDB deck response
---@param slots The slot list for cards in this deck. Table key is the cardId, value is the
--- number of those cards which will be spawned
internal.addInvestigatorCards = function(deck, slots)
local investigatorId = deck.investigator_code
slots[investigatorId .. "-m"] = 1
local deckMeta = JSON.decode(deck.meta)
local parallelFront = deckMeta ~= nil and deckMeta.alternate_front ~= nil and deckMeta.alternate_front ~= ""
local parallelBack = deckMeta ~= nil and deckMeta.alternate_back ~= nil and deckMeta.alternate_back ~= ""
if parallelFront and parallelBack then
investigatorId = investigatorId .. "-p"
elseif parallelFront then
local alternateNum = tonumber(deckMeta.alternate_front)
if alternateNum >= 01501 and alternateNum <= 01506 then
investigatorId = investigatorId .. "-r"
else
investigatorId = investigatorId .. "-pf"
end
elseif parallelBack then
investigatorId = investigatorId .. "-pb"
end
slots[investigatorId] = 1
end
-- Process the card list looking for the customizable cards, and add their upgrade sheets if needed
---@param slots The slot list for cards in this deck. Table key is the cardId, value is the number
-- of those cards which will be spawned
internal.maybeAddCustomizeUpgradeSheets = function(slots)
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
for cardId, _ in pairs(slots) do
-- upgrade sheets for customizable cards
local upgradesheet = allCardsBag.call("getCardById", { id = cardId .. "-c" })
if upgradesheet ~= nil then
slots[cardId .. "-c"] = 1
end
end
end
-- Process the card list looking for the Summoned Servitor, and add its minicard to the list if
-- needed
---@param slots The slot list for cards in this deck. Table key is the cardId, value is the number
-- of those cards which will be spawned
internal.maybeAddSummonedServitor = function(slots)
if slots["09080"] ~= nil then
slots["09080-m"] = 1
end
end
-- On the Mend should have 1-per-investigator copies set aside, but ArkhamDB always sends 1. Update
-- the count based on the investigator count
---@param slots The slot list for cards in this deck. Table key is the cardId, value is the number
-- of those cards which will be spawned
---@param playerColor Color name of the player this deck is being loaded for. Used for broadcast if an error occurs
internal.maybeAddOnTheMend = function(slots, playerColor)
if slots["09006"] ~= nil then
local investigatorCount = playAreaApi.getInvestigatorCount()
if investigatorCount ~= nil then
slots["09006"] = investigatorCount
else
printToAll("Something went wrong with the load, adding 4 copies of On the Mend", playerColor)
slots["09006"] = 4
end
end
end
-- Process the slot list and looks for any cards which are bonded to those in the deck. Adds those cards to the slot list.
---@param slots The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
internal.extractBondedCards = function(slots)
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
-- Create a list of bonded cards first so we don't modify slots while iterating
local bondedCards = { }
local bondedList = { }
for cardId, cardCount in pairs(slots) do
local card = allCardsBag.call("getCardById", { id = cardId })
if (card ~= nil and card.metadata.bonded ~= nil) then
for _, bond in ipairs(card.metadata.bonded) do
bondedCards[bond.id] = bond.count
-- We need to know which cards are bonded to determine their position, remember them
bondedList[bond.id] = true
end
end
end
-- Add any bonded cards to the main slots list
for bondedId, bondedCount in pairs(bondedCards) do
slots[bondedId] = bondedCount
end
return bondedList
end
-- 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")
---@param tabooId The deck's taboo ID, taken from the deck response taboo_id field. May be nil, indicating that no taboo list should be used
---@param slots The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
internal.checkTaboos = function(tabooId, slots, playerColor)
if tabooId then
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
for cardId, _ in pairs(tabooList[tabooId].cards) do
if slots[cardId] ~= nil then
-- Make sure there's a taboo version of the card before we replace it
-- SCED only maintains the most recent taboo cards. If a deck is using
-- an older taboo list it's possible the card isn't a taboo any more
local tabooCard = allCardsBag.call("getCardById", { id = cardId .. "-t" })
if tabooCard == nil then
local basicCard = allCardsBag.call("getCardById", { id = cardId })
printToAll("Taboo version for " .. basicCard.data.Nickname .. " is not available. Using standard version", playerColor)
else
slots[cardId .. "-t"] = slots[cardId]
slots[cardId] = nil
end
end
end
end
end
-- Gets the ArkhamDB config info from the configuration object.
---@return Table. Configuration data
internal.getConfiguration = function()
local configuration = getObjectsWithTag("import_configuration_provider")[1]:getTable("configuration")
printPriority = configuration.priority
return configuration
end
internal.fixUtf16String = function(str)
return str:gsub("\\u(%w%w%w%w)", function(match)
return string.char(tonumber(match, 16))
end)
end
---@type Request
Request = {
is_done = false,
is_successful = false
}
-- Creates a new instance of a Request. Should not be directly called. Instead use Request.start and Request.deferred.
---@param uri string
---@param configure fun(request: Request, status: WebRequestStatus)
---@return Request
function Request:new(uri, configure)
local this = {}
setmetatable(this, self)
self.__index = self
if type(uri) == "table" then
uri = table.concat(uri, "/")
end
this.uri = uri
WebRequest.get(uri, function(status)
configure(this, status)
end)
return this
end
-- Creates a new request. on_success should set the request's is_done, is_successful, and content variables.
-- 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)
---@param uri string
---@param on_success fun(request: Request, status: WebRequestStatus, vararg any)
---@param on_error fun(status: WebRequestStatus)|nil
---@vararg any[]
---@return Request
function Request.deferred(uri, on_success, on_error, ...)
local parameters = table.pack(...)
return Request:new(uri, function(request, status)
if (status.is_done) then
if (status.is_error) then
request.error_message = on_error and on_error(status, table.unpack(parameters)) or status.error
request.is_successful = false
request.is_done = true
else
on_success(request, status)
end
end
end)
end
-- Creates a new request. on_success should return weather the resultant data is as expected, and the processed content of the request.
---@param uri string
---@param on_success fun(status: WebRequestStatus, vararg any): boolean, any
---@param on_error nil|fun(status: WebRequestStatus, vararg any): string
---@vararg any[]
---@return Request
function Request.start(uri, on_success, on_error, ...)
local parameters = table.pack(...)
return Request.deferred(uri, function(request, status)
local result, message = on_success(status, table.unpack(parameters))
if not result then request.error_message = message else request.content = message end
request.is_successful = result
request.is_done = true
end, on_error, table.unpack(parameters))
end
---@param requests Request[]
---@param on_success fun(content: any[], vararg any[])
---@param on_error fun(requests: Request[], vararg any[])|nil
---@vararg any
function Request.with_all(requests, on_success, on_error, ...)
local parameters = table.pack(...)
Wait.condition(function()
---@type any[]
local results = {}
---@type Request[]
local errors = {}
for _, request in ipairs(requests) do
if request.is_successful then
table.insert(results, request.content)
else
table.insert(errors, request)
end
end
if (#errors <= 0) then
on_success(results, table.unpack(parameters))
elseif on_error == nil then
for _, request in ipairs(errors) do
printToAll(table.concat({ "[ERROR]", request.uri, ":", request.error_message }))
end
else
on_error(requests, table.unpack(parameters))
end
end, function()
for _, request in ipairs(requests) do
if not request.is_done then return false end
end
return true
end)
end
---@param callback fun(content: any, vararg any)
function Request:with(callback, ...)
local arguments = table.pack(...)
Wait.condition(function()
if self.is_successful then
callback(self.content, table.unpack(arguments))
end
end, function() return self.is_done
end)
end
return ArkhamDb
end

View File

@ -1,10 +1,14 @@
require("arkhamdb/LoaderUi")
require("arkhamdb/DeckImporterUi")
require("playercards/PlayerCardSpawner")
local playAreaApi = require("core/PlayAreaApi")
local playAreaApi = require("core/PlayAreaApi")
local arkhamDb = require("arkhamdb/ArkhamDb")
local zones = require("playermat/Zones")
local bondedList = { }
local DEBUG = false
local ALL_CARDS_GUID = "15bb07"
local customizationRowsWithFields = { }
-- inputMap maps from (our 1-indexes) customization row index to inputValue table index
-- The Raven Quill
@ -34,294 +38,26 @@ customizationRowsWithFields["09101"].inputMap[1] = 1
customizationRowsWithFields["09101"].inputMap[2] = 2
customizationRowsWithFields["09101"].inputMap[3] = 3
local RANDOM_WEAKNESS_ID = "01000"
local tags = { configuration = "import_configuration_provider" }
local Priority = {
ERROR = 0,
WARNING = 1,
INFO = 2,
DEBUG = 3
}
---@type fun(text: string)
local printFunction = printToAll
local printPriority = Priority.INFO
---@param priority number
---@return string
function Priority.getLabel(priority)
if priority == 0 then return "ERROR"
elseif priority == 1 then return "WARNING"
elseif priority == 2 then return "INFO"
elseif priority == 3 then return "DEBUG"
else error(table.concat({ "Priority", priority, "not found" }, " ")) return ""
end
end
---@param message string
---@param priority number
local function debugPrint(message, priority, color)
if (color == nil) then
color = { 0.5, 0.5, 0.5 }
end
if (printPriority >= priority) then
printFunction("[" .. Priority.getLabel(priority) .. "] " .. message, color)
end
end
local function fixUtf16String(str)
return str:gsub("\\u(%w%w%w%w)", function(match)
return string.char(tonumber(match, 16))
end)
end
--Forward declaration
---@type Request
local Request = {}
---@type table<string, ArkhamImportTaboo>
local tabooList = {}
---@return ArkhamImportConfiguration
local function getConfiguration()
local configuration = getObjectsWithTag(tags.configuration)[1]:getTable("configuration")
printPriority = configuration.priority
return configuration
end
function onLoad(script_state)
local state = JSON.decode(script_state)
initializeUi(state)
math.randomseed(os.time())
local configuration = getConfiguration()
Request.start({ configuration.api_uri, configuration.taboo }, function(status)
local json = JSON.decode(fixUtf16String(status.text))
for _, taboo in pairs(json) do
---@type <string, boolean>
local cards = {}
for _, card in pairs(JSON.decode(taboo.cards)) do
cards[card.code] = true
end
tabooList[taboo.id] = {
date = taboo.date_start,
cards = cards
}
end
return true, nil
end)
arkhamDb.initialize()
end
function onSave() return JSON.encode(getUiState()) end
-- Callback when the deck information is received from ArkhamDB. Parses the
-- response then applies standard transformations to the deck such as adding
-- random weaknesses and checking for taboos. Once the deck is processed,
-- passes to loadCards to actually spawn the defined deck.
---@param deck ArkhamImportDeck
---@param playerColor String Color name of the player mat to place this deck on (e.g. "Red")
---@param configuration ArkhamImportConfiguration
local function onDeckResult(deck, playerColor, configuration)
-- Load the next deck in the upgrade path if the option is enabled
if (getUiState().loadNewest and deck.next_deck ~= nil and deck.next_deck ~= "") then
buildDeck(playerColor, deck.next_deck)
return
end
debugPrint(table.concat({ "Found decklist: ", deck.name }), Priority.INFO, playerColor)
debugPrint(table.concat({ "-", deck.name, "-" }), Priority.DEBUG)
for k, v in pairs(deck) do
if type(v) == "table" then
debugPrint(table.concat { k, ": <table>" }, Priority.DEBUG)
else
debugPrint(table.concat { k, ": ", tostring(v) }, Priority.DEBUG)
end
end
debugPrint("", Priority.DEBUG)
-- Initialize deck slot table and perform common transformations. The order
-- of these should not be changed, as later steps may act on cards added in
-- each. For example, a random weakness or investigator may have bonded
-- cards or taboo entries, and should be present
local slots = deck.slots
maybeDrawRandomWeakness(slots, playerColor, configuration)
maybeAddInvestigatorCards(deck, slots)
maybeAddCustomizeUpgradeSheets(slots, configuration)
maybeAddSummonedServitor(slots)
maybeAddOnTheMend(slots, playerColor)
extractBondedCards(slots, configuration)
checkTaboos(deck.taboo_id, slots, playerColor, configuration)
local commandManager = getObjectFromGUID(configuration.command_manager_guid)
---@type ArkhamImport_CommandManager_InitializationArguments
local parameters = {
configuration = configuration,
description = deck.description_md,
}
---@type ArkhamImport_CommandManager_InitializationResults
local results = commandManager:call("initialize", parameters)
if not results.is_successful then
debugPrint(results.error_message, Priority.ERROR)
return
end
-- get upgrades for customizable cards
local meta = deck.meta
local customizations = {}
if meta then customizations = JSON.decode(deck.meta) end
loadCards(slots, deck.investigator_code, customizations, playerColor, commandManager,
configuration, results.configuration)
end
-- Checks to see if the slot list includes the random weakness ID. If it does,
-- removes it from the deck and replaces it with the ID of a random basic weakness provided by the all cards bag
---@param slots: The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
---@param playerColor: Color name of the player this deck is being loaded for. Used for broadcast if a weakness is added.
---@param configuration: The API configuration object
function maybeDrawRandomWeakness(slots, playerColor, configuration)
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
local hasRandomWeakness = false
for cardId, cardCount in pairs(slots) do
if cardId == RANDOM_WEAKNESS_ID then
hasRandomWeakness = true
break
end
end
if hasRandomWeakness then
local weaknessId = allCardsBag.call("getRandomWeaknessId")
slots[weaknessId] = 1
slots[RANDOM_WEAKNESS_ID] = nil
debugPrint("Random basic weakness added to deck", Priority.INFO, playerColor)
end
end
-- If investigator cards should be loaded, add both the investigator (XXXXX) and minicard (XXXXX-m) slots with one copy each
---@param deck: The processed ArkhamDB deck response
---@param slots: The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
function maybeAddInvestigatorCards(deck, slots)
if getUiState().investigators then
local investigatorId = deck.investigator_code
slots[investigatorId .. "-m"] = 1
local deckMeta = JSON.decode(deck.meta)
local parallelFront = deckMeta ~= nil and deckMeta.alternate_front ~= nil and deckMeta.alternate_front ~= ""
local parallelBack = deckMeta ~= nil and deckMeta.alternate_back ~= nil and deckMeta.alternate_back ~= ""
if parallelFront and parallelBack then
investigatorId = investigatorId .. "-p"
elseif parallelFront then
local alternateNum = tonumber(deckMeta.alternate_front)
if alternateNum >= 01501 and alternateNum <= 01506 then
investigatorId = investigatorId .. "-r"
else
investigatorId = investigatorId .. "-pf"
end
elseif parallelBack then
investigatorId = investigatorId .. "-pb"
end
slots[investigatorId] = 1
end
end
-- Process the card list looking for the customizable cards, and add their upgrade sheets if needed
---@param slots: The slot list for cards in this deck. Table key is the cardId, value is the number
-- of those cards which will be spawned
function maybeAddCustomizeUpgradeSheets(slots, configuration)
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
for cardId, _ in pairs(slots) do
-- upgrade sheets for customizable cards
local upgradesheet = allCardsBag.call("getCardById", { id = cardId .. "-c" })
if upgradesheet ~= nil then
slots[cardId .. "-c"] = 1
end
end
end
-- Process the card list looking for the Summoned Servitor, and add its minicard to the list if
-- needed
---@param slots: The slot list for cards in this deck. Table key is the cardId, value is the number
-- of those cards which will be spawned
function maybeAddSummonedServitor(slots)
if slots["09080"] ~= nil then
slots["09080-m"] = 1
end
end
-- On the Mend should have 1-per-investigator copies set aside, but ArkhamDB always sends 1. Update
-- the count based on the investigator count
---@param slots: The slot list for cards in this deck. Table key is the cardId, value is the number
-- of those cards which will be spawned
---@param playerColor: Color name of the player this deck is being loaded for. Used for broadcast if an error occurs
function maybeAddOnTheMend(slots, playerColor)
if slots["09006"] ~= nil then
local investigatorCount = playAreaApi.getInvestigatorCount()
if investigatorCount ~= nil then
slots["09006"] = investigatorCount
else
debugPrint("Something went wrong with the load, adding 4 copies of On the Mend", Priority.INFO, playerColor)
slots["09006"] = 4
end
end
end
-- Process the slot list and looks for any cards which are bonded to those in the deck. Adds those cards to the slot list.
---@param slots: The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
---@param configuration: The API configuration object
function extractBondedCards(slots, configuration)
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
-- Create a list of bonded cards first so we don't modify slots while iterating
local bondedCards = {}
for cardId, cardCount in pairs(slots) do
local card = allCardsBag.call("getCardById", { id = cardId })
if (card ~= nil and card.metadata.bonded ~= nil) then
for _, bond in ipairs(card.metadata.bonded) do
bondedCards[bond.id] = bond.count
-- We need to know which cards are bonded to determine their position, remember them
bondedList[bond.id] = true
end
end
end
-- Add any bonded cards to the main slots list
for bondedId, bondedCount in pairs(bondedCards) do
slots[bondedId] = bondedCount
end
end
-- 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")
---@param tabooId: The deck's taboo ID, taken from the deck response taboo_id field. May be nil, indicating that no taboo list should be used
---@param slots: The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
function checkTaboos(tabooId, slots, playerColor, configuration)
if tabooId then
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
for cardId, _ in pairs(tabooList[tabooId].cards) do
if slots[cardId] ~= nil then
-- Make sure there's a taboo version of the card before we replace it
-- SCED only maintains the most recent taboo cards. If a deck is using
-- an older taboo list it's possible the card isn't a taboo any more
local tabooCard = allCardsBag.call("getCardById", { id = cardId .. "-t" })
if tabooCard == nil then
local basicCard = allCardsBag.call("getCardById", { id = cardId })
debugPrint("Taboo version for " .. basicCard.data.Nickname .. " is not available. Using standard version",
Priority.WARNING, playerColor)
else
slots[cardId .. "-t"] = slots[cardId]
slots[cardId] = nil
end
end
end
end
end
-- Returns the zone name where the specified card should be placed, based on its metadata.
---@param cardMetadata: Table of card metadata. Metadata fields type and permanent are required; all others are optional.
---@return: Zone name such as "Deck", "SetAside1", etc. See Zones object documentation for a list of valid zones.
function getDefaultCardZone(cardMetadata)
---@param cardMetadata Table of card metadata.
---@return Zone name such as "Deck", "SetAside1", etc. See Zones object documentation for a list of
--- valid zones.
function getDefaultCardZone(cardMetadata, bondedList)
if (cardMetadata.id == "09080-m") then -- Have to check the Servitor before other minicards
return "SetAside6"
elseif (cardMetadata.id == "09006") then -- On The Mend is set aside
@ -344,27 +80,41 @@ function getDefaultCardZone(cardMetadata)
end
end
-- Process the slot list, which defines the card Ids and counts of cards to load. Spawn those cards at the appropriate zones
-- and report an error to the user if any could not be loaded.
function buildDeck(playerColor, deckId)
local uiState = getUiState()
arkhamDb.getDecklist(
playerColor,
deckId,
uiState.private,
uiState.loadNewest,
uiState.investigators,
loadCards)
end
-- Process the slot list, which defines the card Ids and counts of cards to load. Spawn those cards
-- at the appropriate zones and report an error to the user if any could not be loaded.
-- This is a callback function which handles the results of ArkhamDb.getDecklist()
-- This method uses an encapsulated coroutine with yields to make the card spawning cleaner.
--
---@param slots: Key-Value table of cardId:count. cardId is the ArkhamDB ID of the card to spawn, and count is the number which should be spawned
---@param investigatorId: String ArkhamDB ID (code) for this deck's investigator.
---@param slots Key-Value table of cardId:count. cardId is the ArkhamDB ID of the card to spawn,
--- and count is the number which should be spawned
---@param investigatorId String ArkhamDB ID (code) for this deck's investigator.
-- Investigator cards should already be added to the slots list if they
-- should be spawned, but this value is separate to check for special
-- handling for certain investigators
---@param customizations: ArkhamDB data for customizations on customizable cards
---@param bondedList A table of cardID keys to meaningless values. Card IDs in this list were added
--- from a parent bonded card.
---@param customizations ArkhamDB data for customizations on customizable cards
---@param playerColor String Color name of the player mat to place this deck on (e.g. "Red")
---@param configuration: Loader configuration object
function loadCards(slots, investigatorId, customizations, playerColor, commandManager, configuration, command_config)
function loadCards(slots, investigatorId, bondedList, customizations, playerColor)
function coinside()
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
local allCardsBag = getObjectFromGUID(ALL_CARDS_GUID)
local yPos = {}
local cardsToSpawn = {}
for cardId, cardCount in pairs(slots) do
local card = allCardsBag.call("getCardById", { id = cardId })
if card ~= nil then
local cardZone = getDefaultCardZone(card.metadata)
local cardZone = getDefaultCardZone(card.metadata, bondedList)
for i = 1, cardCount do
table.insert(cardsToSpawn, { data = card.data, metadata = card.metadata, zone = cardZone })
end
@ -373,12 +123,6 @@ function loadCards(slots, investigatorId, customizations, playerColor, commandMa
end
end
-- TODO: Re-enable this later, as a command
-- handleAltInvestigatorCard(cardsToSpawn, "promo", configuration)
-- TODO: Process commands for the cardsToSpawn list
-- These should probably be commands, once the command handler is updated
handleAncestralKnowledge(cardsToSpawn)
handleUnderworldMarket(cardsToSpawn, playerColor)
handleHunchDeck(investigatorId, cardsToSpawn, playerColor)
@ -422,27 +166,11 @@ function loadCards(slots, investigatorId, customizations, playerColor, commandMa
for cardId, remainingCount in pairs(slots) do
if remainingCount > 0 then
hadError = true
local request = Request.start({
configuration.api_uri,
configuration.cards,
cardId
},
function(result)
local adbCardInfo = JSON.decode(fixUtf16String(result.text))
local cardName = adbCardInfo.real_name
if (cardName ~= nil) then
if (adbCardInfo.xp ~= nil and adbCardInfo.xp > 0) then
cardName = cardName .. " (" .. adbCardInfo.xp .. ")"
end
debugPrint("Card not found: " .. cardName .. ", ArkhamDB ID " .. cardId, Priority.ERROR, playerColor)
else
debugPrint("Card not found in ArkhamDB, ID " .. cardId, Priority.ERROR, playerColor)
end
end)
arkhamDb.logCardNotFound(cardId, playerColor)
end
end
if (not hadError) then
debugPrint("Deck loaded successfully!", Priority.INFO, playerColor)
printToAll("Deck loaded successfully!", playerColor)
end
return 1
end
@ -509,37 +237,8 @@ function buildZoneLists(cards)
return zoneList
end
-- Replace the investigator card and minicard with an alternate version. This
-- will find the relevant cards and look for IDs with <id>-<altVersionTag>, and
-- <id>-<altVersionTag>-m, and update the entries in cardList with the new card
-- data.
--
---@param cardList: Deck list being created
---@param altVersionTag: The tag for the different version, currently the only alt versions are "promo", but will soon inclide "revised"
---@param configuration: ArkhamDB configuration defniition, used for the card bag
function handleAltInvestigatorCard(cardList, altVersionTag, configuration)
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
for _, card in ipairs(cardList) do
if card.metadata.type == "Investigator" then
local altInvestigator = allCardsBag.call("getCardById", { id = card.metadata.id .. "-" .. altVersionTag })
if (altInvestigator ~= nil) then
card.data = altInvestigator.data
card.metadata = altInvestigator.metadata
end
end
if card.metadata.type == "Minicard" then
-- -promo comes before -m in the ID, so needs a little massaging
local investigatorId = string.sub(card.metadata.id, 1, 5)
local altMinicard = allCardsBag.call("getCardById", { id = investigatorId .. "-" .. altVersionTag .. "-m" })
if altMinicard ~= nil then
card.data = altMinicard.data
card.metadata = altMinicard.metadata
end
end
end
end
-- Check to see if the deck list has Ancestral Knowledge. If it does, move 5 random skills to SetAside3
---@param cardList Deck list being created
function handleAncestralKnowledge(cardList)
local hasAncestralKnowledge = false
local skillList = {}
@ -565,8 +264,8 @@ function handleAncestralKnowledge(cardList)
end
-- Check for and handle Underworld Market by moving all Illicit cards to UnderSetAside3
---@param cardList: Deck list being created
---@param playerColor: Color this deck is being loaded for
---@param cardList Deck list being created
---@param playerColor Color this deck is being loaded for
function handleUnderworldMarket(cardList, playerColor)
local hasMarket = false
local illicitList = {}
@ -585,8 +284,9 @@ function handleUnderworldMarket(cardList, playerColor)
if hasMarket then
if #illicitList < 10 then
debugPrint("Only " .. #illicitList .. " Illicit cards in your deck, you can't trigger Underworld Market's ability."
, Priority.WARNING, playerColor)
printToAll("Only " .. #illicitList ..
" Illicit cards in your deck, you can't trigger Underworld Market's ability.",
playerColor)
else
-- Process cards to move them to the market deck. This is done in reverse
-- order because the sorting needs to be reversed (deck sorts for face down)
@ -601,19 +301,22 @@ function handleUnderworldMarket(cardList, playerColor)
end
if #illicitList > 10 then
debugPrint("Moved all " .. #illicitList .. " Illicit cards to the Market deck, reduce it to 10", Priority.INFO,
printToAll("Moved all " .. #illicitList ..
" Illicit cards to the Market deck, reduce it to 10",
playerColor)
else
debugPrint("Built the Market deck", Priority.INFO, playerColor)
printToAll("Built the Market deck", playerColor)
end
end
end
end
-- If the investigator is Joe Diamond, extract all Insight events to SetAside5 to build the Hunch Deck.
---@param investigatorId: ID for the deck's investigator card. Passed separately because the investigator may not be included in the cardList
---@param cardList: Deck list being created
---@param playerColor: Color this deck is being loaded for
-- If the investigator is Joe Diamond, extract all Insight events to SetAside5 to build the Hunch
-- Deck.
---@param investigatorId ID for the deck's investigator card. Passed separately because the
--- investigator may not be included in the cardList
---@param cardList Deck list being created
---@param playerColor Color this deck is being loaded for
function handleHunchDeck(investigatorId, cardList, playerColor)
if investigatorId == "05002" then -- Joe Diamond
local insightList = {}
@ -637,21 +340,21 @@ function handleHunchDeck(investigatorId, cardList, playerColor)
table.insert(cardList, moving)
end
if #insightList < 11 then
debugPrint("Joe's hunch deck must have 11 cards but the deck only has " .. #insightList .. " Insight events.",
Priority.INFO, playerColor)
printToAll("Joe's hunch deck must have 11 cards but the deck only has " .. #insightList ..
" Insight events.", playerColor)
elseif #insightList > 11 then
debugPrint("Moved all " .. #insightList .. " Insight events to the hunch deck, reduce it to 11.", Priority.INFO,
playerColor)
printToAll("Moved all " .. #insightList ..
" Insight events to the hunch deck, reduce it to 11.", playerColor)
else
debugPrint("Built Joe's hunch deck", Priority.INFO, playerColor)
printToAll("Built Joe's hunch deck", playerColor)
end
end
end
-- For any customization upgrade cards in the card list, process the metadata from the deck to
-- set the save state to show the correct checkboxes/text field values
---@param cardList: Deck list being created
---@param customizations: Deck's meta table, extracted from ArkhamDB's deck structure
---@param cardList Deck list being created
---@param customizations Deck's meta table, extracted from ArkhamDB's deck structure
function handleCustomizableUpgrades(cardList, customizations)
for _, card in ipairs(cardList) do
if card.metadata.type == "UpgradeSheet" then
@ -714,189 +417,6 @@ function handleCustomizableUpgrades(cardList, customizations)
end
end
-- Test method. Loads all decks which were submitted to ArkhamDB on a given date window.
function testLoadLotsOfDecks()
local configuration = getConfiguration()
local numDays = 7
local day = os.time { year = 2021, month = 7, day = 15 } -- Start date here
for i = 1, numDays do
local dateString = os.date("%Y-%m-%d", day)
local deckList = Request.start({
configuration.api_uri,
"decklists/by_date",
dateString,
},
function(result)
local json = JSON.decode(result.text)
for i, deckData in ipairs(json) do
buildDeck(getColorForTest(i), deckData.id)
end
end)
day = day + (60 * 60 * 24) -- Move forward by one day
end
end
-- Rotates the player mat based on index, to spread the card stacks during a mass load
function getColorForTest(index)
if (index % 4 == 0) then
return "Red"
elseif (index % 4 == 1) then
return "Orange"
elseif (index % 4 == 2) then
return "White"
elseif (index % 4 == 3) then
return "Green"
end
end
-- Start the deck build process for the given player color and deck ID. This
-- will retrieve the deck from ArkhamDB, and pass to a callback for processing.
---@param playerColor String Color name of the player mat to place this deck on (e.g. "Red")
---@param deckId: ArkhamDB deck id to be loaded
function buildDeck(playerColor, deckId)
local configuration = getConfiguration()
-- Get a simple card to see if the bag indexes are complete. If not, abort
-- the deck load. The called method will handle player notification.
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
local checkCard = allCardsBag.call("getCardById", { id = "01001" })
if (checkCard ~= nil and checkCard.data == nil) then
return
end
local deckUri = { configuration.api_uri,
getUiState().private and configuration.private_deck or configuration.public_deck, deckId }
local deck = Request.start(deckUri, function(status)
if string.find(status.text, "<!DOCTYPE html>") then
debugPrint("Private deck ID " .. deckId .. " is not shared", Priority.ERROR, playerColor)
return false, table.concat({ "Private deck ", deckId, " is not shared" })
end
local json = JSON.decode(status.text)
if not json then
debugPrint("Deck ID " .. deckId .. " not found", Priority.ERROR, playerColor)
return false, "Deck not found!"
end
return true, JSON.decode(status.text)
end)
deck:with(onDeckResult, playerColor, configuration)
end
---@type Request
Request = {
is_done = false,
is_successful = false
}
-- Creates a new instance of a Request. Should not be directly called. Instead use Request.start and Request.deferred.
---@param uri string
---@param configure fun(request: Request, status: WebRequestStatus)
---@return Request
function Request:new(uri, configure)
local this = {}
setmetatable(this, self)
self.__index = self
if type(uri) == "table" then
uri = table.concat(uri, "/")
end
this.uri = uri
WebRequest.get(uri, function(status)
configure(this, status)
end)
return this
end
-- Creates a new request. on_success should set the request's is_done, is_successful, and content variables.
-- 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)
---@param uri string
---@param on_success fun(request: Request, status: WebRequestStatus, vararg any)
---@param on_error fun(status: WebRequestStatus)|nil
---@vararg any[]
---@return Request
function Request.deferred(uri, on_success, on_error, ...)
local parameters = table.pack(...)
return Request:new(uri, function(request, status)
if (status.is_done) then
if (status.is_error) then
request.error_message = on_error and on_error(status, table.unpack(parameters)) or status.error
request.is_successful = false
request.is_done = true
else
on_success(request, status)
end
end
end)
end
-- Creates a new request. on_success should return weather the resultant data is as expected, and the processed content of the request.
---@param uri string
---@param on_success fun(status: WebRequestStatus, vararg any): boolean, any
---@param on_error nil|fun(status: WebRequestStatus, vararg any): string
---@vararg any[]
---@return Request
function Request.start(uri, on_success, on_error, ...)
local parameters = table.pack(...)
return Request.deferred(uri, function(request, status)
local result, message = on_success(status, table.unpack(parameters))
if not result then request.error_message = message else request.content = message end
request.is_successful = result
request.is_done = true
end, on_error, table.unpack(parameters))
end
---@param requests Request[]
---@param on_success fun(content: any[], vararg any[])
---@param on_error fun(requests: Request[], vararg any[])|nil
---@vararg any
function Request.with_all(requests, on_success, on_error, ...)
local parameters = table.pack(...)
Wait.condition(function()
---@type any[]
local results = {}
---@type Request[]
local errors = {}
for _, request in ipairs(requests) do
if request.is_successful then
table.insert(results, request.content)
else
table.insert(errors, request)
end
end
if (#errors <= 0) then
on_success(results, table.unpack(parameters))
elseif on_error == nil then
for _, request in ipairs(errors) do
debugPrint(table.concat({ "[ERROR]", request.uri, ":", request.error_message }), Priority.ERROR)
end
else
on_error(requests, table.unpack(parameters))
end
end, function()
for _, request in ipairs(requests) do
if not request.is_done then return false end
end
return true
end)
end
---@param callback fun(content: any, vararg any)
function Request:with(callback, ...)
local arguments = table.pack(...)
Wait.condition(function()
if self.is_successful then
callback(self.content, table.unpack(arguments))
end
end, function() return self.is_done
end)
function log(message)
if DEBUG then print(message) end
end

View File

@ -675,6 +675,8 @@ function applyOptionPanelChange(id, state)
elseif id == 3 then
playmatAPI.clickableClues(state, "All")
-- update master clue counter
getObjectFromGUID("4a3aa4").setVar("useClickableCounters", state)
end
end

View File

@ -1,16 +1,17 @@
local clueCounters = {}
local clueCounterGUIDS = {
"37be78",
"1769ed",
"032300",
"d86b7c"
}
count = 0
useClickableCounters = false
function onLoad()
local playmatAPI = require("playermat/PlaymatApi")
function onSave() return JSON.encode(useClickableCounters) end
function onLoad(savedData)
if savedData ~= nil then
useClickableCounters = JSON.decode(savedData)
end
self.createButton({
label = "0",
click_function = "removeAllPlayerClues",
tooltip = "Click here to remove all Clues from playermats",
tooltip = "Click here to remove all collected clues",
function_owner = self,
position = { 0, 0.06, 0 },
height = 900,
@ -21,26 +22,18 @@ function onLoad()
color = { 0, 0, 0, 0 }
})
-- loading object references to the counting bowls via GUID
for i = 1, 4 do
clueCounters[i] = getObjectFromGUID(clueCounterGUIDS[i])
end
loopID = Wait.time(sumClues, 2, -1)
end
-- removes all player clues by calling the respective function from the counting bowls
-- removes all player clues by calling the respective function from the counting bowls / clickers
function removeAllPlayerClues()
for i = 1, 4 do
clueCounters[i].call("removeAllClues")
end
printToAll(count .. " clue(s) from playermats removed.", "White")
playmatAPI.removeClues("All")
self.editButton({ index = 0, label = "0" })
end
-- gets the counted values from the counting bowls and sums them up
-- gets the counted values from the counting bowls / clickers and sums them up
function sumClues()
local count = 0
for i = 1, 4 do
count = count + tonumber(clueCounters[i].getVar("exposedValue"))
end
count = playmatAPI.getClueCount(useClickableCounters, "All")
self.editButton({ index = 0, label = tostring(count) })
end

View File

@ -5,11 +5,11 @@ do
local internal = { }
-- Base IDs for various tour card UI elements. Actual IDs will have _[playerColor] appended
local cardId = "tourCard"
local narratorId = "tourNarratorImage"
local textId = "tourText"
local nextButtonId = "tourNext"
local stopButtonId = "tourStop"
local CARD_ID = "tourCard"
local NARRATOR_ID = "tourNarratorImage"
local TEXT_ID = "tourText"
local NEXT_BUTTON_ID = "tourNext"
local STOP_BUTTON_ID = "tourStop"
-- Table centerpoint for the camera hook object. Camera handling is a bit erratic so it doesn't
-- always land right where you think it's going to, but it's close
@ -19,107 +19,117 @@ do
z = 0,
}
local cameraHookGuid
local currentCardIndex
-- Tracks the current state of the tours. Keyed by player color to keep each player's tour
-- separate, will hold the camera hook and current card.
local tourState = { }
-- Kicks off the tour by initializing the card and camera hook. A callback on the hook creation
-- will then show the first card.
-- @param playerColor Player color to start the tour for
---@param playerColor Player color to start the tour for
TourManager.startTour = function(playerColor)
tourState[playerColor] = {
currentCardIndex = 1
-- Camera gets really screwy when we finalize if we don't start in ThirdPerson before attaching
-- to the hook
Player["White"].setCameraMode("ThirdPerson")
internal.createTourCard("White")
-- XML update takes time to load, wait for it to finish then create the hook
}
-- Camera gets really screwy when we finalize if we don't start settled in ThirdPerson at the
-- default position before attaching to the hook. Unfortunately there are no callbacks for when
-- the movement is done, but the 2 sec seems to handle it
Player[playerColor].setCameraMode("ThirdPerson")
Player[playerColor].lookAt({position={-22.265,-2.5,5.2575},pitch=64.343,yaw=90.333,distance=104.7})
Wait.time(function()
internal.createTourCard(playerColor)
-- XML update to add the new card takes a few frames to load, wait for it to finish then
-- create the hook
Wait.condition(
function()
internal.createCameraHook()
internal.createCameraHook(playerColor)
end,
function()
return not Global.UI.loading
end
)
end, 2)
end
-- Shows the next card in the tour script. This method is exposed (rather than being part of
-- internal) because the XMLUI callbacks expect the method to be on the object directly.
-- @param playerColor Player color to show the next card for
function nextCard(playerColor)
internal.hideCard()
---@param player Player object to show the next card for, provided by XMLUI callback
function nextCard(player)
internal.hideCard(player.color)
Wait.time(function()
currentCardIndex = currentCardIndex + 1
if currentCardIndex > #TOUR_SCRIPT then
internal.finalizeTour()
tourState[player.color].currentCardIndex = tourState[player.color].currentCardIndex + 1
if tourState[player.color].currentCardIndex > #TOUR_SCRIPT then
internal.finalizeTour(player.color)
else
internal.showCurrentCard()
internal.showCurrentCard(player.color)
end
end, 0.3)
end
-- Ends the tour and cleans up the camera. This method is exposed (rather than being part of
-- internal) because the XMLUI callbacks expect the method to be on the object directly.
-- @param playerColor Player color to end the tour for
function stopTour(playerColor)
internal.hideCard()
---@param player Player object to end the tour for, provided by XMLUI callback
function stopTour(player)
internal.hideCard(player.color)
Wait.time(function()
internal.finalizeTour()
internal.finalizeTour(player.color)
end, 0.3)
end
-- Updates the card UI for the script at the current index, moves the camera to the proper
-- position, and shows the card.
-- @param playerColor Player color to show the current card for
---@param playerColor Player color to show the current card for
internal.showCurrentCard = function(playerColor)
internal.updateCardDisplay(currentCardIndex)
local hook = getObjectFromGUID(cameraHookGuid)
internal.updateCardDisplay(playerColor)
local hook = getObjectFromGUID(tourState[playerColor].cameraHookGuid)
hook.setPositionSmooth(CAMERA_HOME, false, false)
local delay = 0.5
if TOUR_SCRIPT[currentCardIndex].showObj ~= nil then
local cardIndex = tourState[playerColor].currentCardIndex
if TOUR_SCRIPT[cardIndex].showObj ~= nil then
Wait.time(function()
local lookAtObj = getObjectFromGUID(TOUR_SCRIPT[currentCardIndex].showObj)
local lookAtObj = getObjectFromGUID(TOUR_SCRIPT[cardIndex].showObj)
hook.setPositionSmooth(lookAtObj.getPosition(), false, false)
end, delay)
delay = delay + 0.5
end
Wait.time(function() Global.UI.show(cardId) end, delay)
Wait.time(function() Global.UI.show(internal.getUiId(CARD_ID, playerColor)) end, delay)
end
-- Hides the current card being shown to a player. This can be in preparation for showing the
-- next card, or ending the tour.
-- @param playerColor Player color to hide the current card for
---@param playerColor Player color to hide the current card for
internal.hideCard = function(playerColor)
Global.UI.hide(cardId)
Global.UI.hide(internal.getUiId(CARD_ID, playerColor))
end
-- Cleans up all the various resources associated with the tour, and (hopefully) resets the
-- camera to the default position. Camera handling is erratic, the final card in the script
-- should include instructions for the player to fix it.
-- @param playerColor Player color to clean up
---@param playerColor Player color to clean up
internal.finalizeTour = function(playerColor)
local cameraHook = getObjectFromGUID(cameraHookGuid)
local cameraHook = getObjectFromGUID(tourState[playerColor].cameraHookGuid)
cameraHook.destruct()
Player["White"].setCameraMode("ThirdPerson")
Player[playerColor].setCameraMode("ThirdPerson")
tourState[playerColor] = nil
Wait.frames(function()
-- This resets to the default camera position. If we don't place the camera exactly at the
-- default, camera controls get weird
Player["White"].lookAt({position={-22.265,-2.5,5.2575},pitch=64.343,yaw=90.333,distance=104.7})
Player[playerColor].lookAt({position={-22.265,-2.5,5.2575},pitch=64.343,yaw=90.333,distance=104.7})
end, 3)
end
-- Updates the card UI to show the appropriate narrator and text.
-- @param index Script entry which should be shown
-- @param playerColor Player color to update card for
internal.updateCardDisplay = function(index, playerColor)
Global.UI.setAttribute(narratorId, "image", TOUR_SCRIPT[index].narrator)
Global.UI.setAttribute(textId, "text", TOUR_SCRIPT[index].text)
---@param playerColor Player color to update card for
internal.updateCardDisplay = function(playerColor)
local index = tourState[playerColor].currentCardIndex
Global.UI.setAttribute(internal.getUiId(NARRATOR_ID, playerColor), "image", TOUR_SCRIPT[index].narrator)
Global.UI.setAttribute(internal.getUiId(TEXT_ID, playerColor), "text", TOUR_SCRIPT[index].text)
end
-- Creates a small, transparent object which the camera will be attached to in order to move the
-- user's view around the table. This should be called only at the beginning of the tour. Once
-- creation is complete the user's camera will be attached to the hook and the first card will be
-- shown.
-- @param playerColor Player color to create the hook for
---@param playerColor Player color to create the hook for
internal.createCameraHook = function(playerColor)
local hookData = {
Name = "BlockSquare",
@ -141,6 +151,7 @@ do
a = 0,
},
Locked = true,
GMNotes = playerColor
}
spawnObjectData({ data = hookData, callback_function = internal.onHookCreated })
@ -148,34 +159,31 @@ do
-- Callback for creation of the camera hook object. Will attach the camera and show the current
-- (presumably first) card.
-- @param hook Created object
---@param hook Created object
internal.onHookCreated = function(hook)
cameraHookGuid = hook.getGUID()
Player.White.attachCameraToObject({
local playerColor = hook.getGMNotes()
tourState[playerColor].cameraHookGuid = hook.getGUID()
Player[playerColor].attachCameraToObject({
object = hook,
offset = { x = -20, y = 30, z = 0 }
})
internal.showCurrentCard()
internal.showCurrentCard(playerColor)
end
-- Creates an XMLUI entry in Global for a player-specific tour card. Dynamically creating this
-- is somewhat complex, but ensures we can properly handle any player color.
-- @param playerColor Player color to create the card for
---@param playerColor Player color to create the card for
internal.createTourCard = function(playerColor)
if Global.UI.getAttributes("cardId_"..playerColor) ~= nil then
-- Make sure the card doesn't exist before we create a new one
if Global.UI.getAttributes(internal.getUiId(CARD_ID, playerColor)) ~= nil then
return
end
cardId = cardId .. "_" .. playerColor
narratorId = narratorId .. "_" .. playerColor
textId = textId .. "_" .. playerColor
nextButtonId = nextButtonId .. "_" .. playerColor
stopButtonId = stopButtonId .. "_" .. playerColor
tourCardTemplate.attributes.id = cardId
tourCardTemplate.attributes.id = internal.getUiId(CARD_ID, playerColor)
tourCardTemplate.attributes.visibility = playerColor
tourCardTemplate.children[1].attributes.id = narratorId
tourCardTemplate.children[2].children[1].attributes.id = textId
tourCardTemplate.children[3].attributes.id = nextButtonId
tourCardTemplate.children[4].attributes.id = stopButtonId
tourCardTemplate.children[1].attributes.id = internal.getUiId(NARRATOR_ID, playerColor)
tourCardTemplate.children[2].children[1].attributes.id = internal.getUiId(TEXT_ID, playerColor)
tourCardTemplate.children[3].attributes.id = internal.getUiId(NEXT_BUTTON_ID, playerColor)
tourCardTemplate.children[4].attributes.id = internal.getUiId(STOP_BUTTON_ID, playerColor)
tourCardTemplate.children[3].attributes.onClick = self.getGUID().."/nextCard"
tourCardTemplate.children[4].attributes.onClick = self.getGUID().."/stopTour"
@ -184,5 +192,9 @@ do
Global.UI.setXmlTable(globalXml)
end
internal.getUiId = function(baseId, playerColor)
return baseId .. "_" .. playerColor
end
return TourManager
end

View File

@ -15,6 +15,7 @@ function onLoad()
label = "",
click_function = "removeAllClues",
function_owner = self,
position = { 0, 0.1, 0 },
height = 0,
width = 0,
font_color = { 0, 0, 0 },

View File

@ -688,10 +688,66 @@ function showDrawButton(visible)
end
end
-- Shows or hides the clickable clue counter for this playmat
---@param showCounters Boolean. Whether the clickable clue counter should be present
function clickableClues(showCounters)
print("dummy function for clue counters")
-- Spawns / destroys a clickable clue counter for this playmat with the correct amount of clues
---@param showCounter Boolean Whether the clickable clue counter should be present
function clickableClues(showCounter)
local CLUE_COUNTER = getObjectFromGUID(CLUE_COUNTER_GUID)
local CLUE_CLICKER = getObjectFromGUID(CLUE_CLICKER_GUID)
local clickerPos = CLUE_CLICKER.getPosition()
local clueCount = 0
if showCounter then
-- current clue count
clueCount = CLUE_COUNTER.getVar("exposedValue")
-- remove clues
CLUE_COUNTER.call("removeAllClues")
-- set value for clue clickers
CLUE_CLICKER.call("updateVal", clueCount)
-- move clue counters up
clickerPos.y = 1.52
CLUE_CLICKER.setPosition(clickerPos)
else
-- current clue count
clueCount = CLUE_CLICKER.getVar("val")
-- move clue counters down
clickerPos.y = 1.3
CLUE_CLICKER.setPosition(clickerPos)
-- spawn clues
local pos = self.positionToWorld({x = -1.12, y = 0.05, z = 0.7})
for i = 1, clueCount do
pos.y = pos.y + 0.045 * i
spawnToken(pos, "clue")
end
end
end
-- removes all clues (moving tokens to the trash and setting counters to 0)
function removeClues()
local CLUE_COUNTER = getObjectFromGUID(CLUE_COUNTER_GUID)
local CLUE_CLICKER = getObjectFromGUID(CLUE_CLICKER_GUID)
CLUE_COUNTER.call("removeAllClues")
CLUE_CLICKER.call("updateVal", 0)
end
-- reports the clue count
---@param useClickableCounters Boolean Controls which type of counter is getting checked
function getClueCount(useClickableCounters)
local count = 0
if useClickableCounters then
local CLUE_CLICKER = getObjectFromGUID(CLUE_CLICKER_GUID)
count = tonumber(CLUE_CLICKER.getVar("val"))
else
local CLUE_COUNTER = getObjectFromGUID(CLUE_COUNTER_GUID)
count = tonumber(CLUE_COUNTER.getVar("exposedValue"))
end
return count
end
-- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes

View File

@ -6,7 +6,21 @@ do
White = "8b081b",
Orange = "bd0ff4",
Green = "383d8b",
Red = "0840d5",
Red = "0840d5"
}
local CLUE_COUNTER_GUIDS = {
White = "37be78",
Orange = "1769ed",
Green = "032300",
Red = "d86b7c"
}
local CLUE_CLICKER_GUIDS = {
White = "db85d6",
Orange = "3f22e5",
Green = "891403",
Red = "4111de"
}
-- Sets the requested playermat's snap points to limit snapping to matching card types or not. If
@ -43,6 +57,25 @@ do
end
end
-- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat
---@param matColor String for one of the active player colors - White, Orange, Green, Red. Also
-- accepts "All" as a special value which will apply the setting to all four mats.
PlaymatApi.removeClues = function(matColor)
for _, mat in ipairs(internal.getMatForColor(matColor)) do
mat.call("removeClues")
end
end
-- Reports the clue count for the requested playermat
---@param useClickableCounters Boolean Controls which type of counter is getting checked
PlaymatApi.getClueCount = function(useClickableCounters, matColor)
local count = 0
for _, mat in ipairs(internal.getMatForColor(matColor)) do
count = count + tonumber(mat.call("getClueCount", useClickableCounters))
end
return count
end
-- Convenience function to look up a mat's object by color, or get all mats.
---@param matColor String for one of the active player colors - White, Orange, Green, Red. Also
-- accepts "All" as a special value which will return all four mats.

View File

@ -1,294 +0,0 @@
function updateSave()
local data_to_save = { ["ml"] = memoryList }
saved_data = JSON.encode(data_to_save)
self.script_state = saved_data
end
function onload(saved_data)
if saved_data ~= "" then
local loaded_data = JSON.decode(saved_data)
--Set up information off of loaded_data
memoryList = loaded_data.ml
else
--Set up information for if there is no saved saved data
memoryList = {}
end
if next(memoryList) == nil then
createSetupButton()
else
createMemoryActionButtons()
end
end
--Beginning Setup
--Make setup button
function createSetupButton()
self.createButton({
label = "Setup",
click_function = "buttonClick_setup",
function_owner = self,
position = { 0, 5, -2 },
rotation = { 0, 0, 0 },
height = 250,
width = 600,
font_size = 150,
color = { 0, 0, 0 },
font_color = { 1, 1, 1 }
})
end
--Triggered by setup button,
function buttonClick_setup()
memoryListBackup = duplicateTable(memoryList)
memoryList = {}
self.clearButtons()
createButtonsOnAllObjects()
createSetupActionButtons()
end
--Creates selection buttons on objects
function createButtonsOnAllObjects()
local howManyButtons = 0
for _, obj in ipairs(getAllObjects()) do
if obj ~= self then
local dummyIndex = howManyButtons
--On a normal bag, the button positions aren't the same size as the bag.
globalScaleFactor = 1.25 * 1 / self.getScale().x
--Super sweet math to set button positions
local selfPos = self.getPosition()
local objPos = obj.getPosition()
local deltaPos = findOffsetDistance(selfPos, objPos, obj)
local objPos = rotateLocalCoordinates(deltaPos, self)
objPos.x = -objPos.x * globalScaleFactor
objPos.y = objPos.y * globalScaleFactor
objPos.z = objPos.z * 4
--Offset rotation of bag
local rot = self.getRotation()
rot.y = -rot.y + 180
--Create function
local funcName = "selectButton_" .. howManyButtons
local func = function() buttonClick_selection(dummyIndex, obj) end
self.setVar(funcName, func)
self.createButton({
click_function = funcName, function_owner = self,
position = objPos, rotation = rot, height = 1000, width = 1000,
color = { 0.75, 0.25, 0.25, 0.6 },
})
howManyButtons = howManyButtons + 1
end
end
end
--Creates submit and cancel buttons
function createSetupActionButtons()
self.createButton({
label = "Cancel", click_function = "buttonClick_cancel", function_owner = self,
position = { 1.5, 5, 2 }, rotation = { 0, 0, 0 }, height = 350, width = 1100,
font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
})
self.createButton({
label = "Submit", click_function = "buttonClick_submit", function_owner = self,
position = { -1.2, 5, 2 }, rotation = { 0, 0, 0 }, height = 350, width = 1100,
font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
})
self.createButton({
label = "Reset", click_function = "buttonClick_reset", function_owner = self,
position = { -3.5, 5, 2 }, rotation = { 0, 0, 0 }, height = 350, width = 800,
font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
})
end
--During Setup
--Checks or unchecks buttons
function buttonClick_selection(index, obj)
local color = { 0, 1, 0, 0.6 }
if memoryList[obj.getGUID()] == nil then
self.editButton({ index = index, color = color })
--Adding pos/rot to memory table
local pos, rot = obj.getPosition(), obj.getRotation()
--I need to add it like this or it won't save due to indexing issue
memoryList[obj.getGUID()] = {
pos = { x = round(pos.x, 4), y = round(pos.y, 4), z = round(pos.z, 4) },
rot = { x = round(rot.x, 4), y = round(rot.y, 4), z = round(rot.z, 4) },
lock = obj.getLock()
}
obj.highlightOn({ 0, 1, 0 })
else
color = { 0.75, 0.25, 0.25, 0.6 }
self.editButton({ index = index, color = color })
memoryList[obj.getGUID()] = nil
obj.highlightOff()
end
end
--Cancels selection process
function buttonClick_cancel()
memoryList = memoryListBackup
self.clearButtons()
if next(memoryList) == nil then
createSetupButton()
else
createMemoryActionButtons()
end
removeAllHighlights()
broadcastToAll("Selection Canceled", { 1, 1, 1 })
end
--Saves selections
function buttonClick_submit()
if next(memoryList) == nil then
broadcastToAll("You cannot submit without any selections.", { 0.75, 0.25, 0.25 })
else
self.clearButtons()
createMemoryActionButtons()
local count = 0
for guid in pairs(memoryList) do
count = count + 1
local obj = getObjectFromGUID(guid)
if obj ~= nil then obj.highlightOff() end
end
broadcastToAll(count .. " Objects Saved", { 1, 1, 1 })
updateSave()
end
end
--Resets bag to starting status
function buttonClick_reset()
memoryList = {}
self.clearButtons()
createSetupButton()
removeAllHighlights()
broadcastToAll("Tool Reset", { 1, 1, 1 })
updateSave()
end
--After Setup
--Creates recall and place buttons
function createMemoryActionButtons()
self.createButton({
label = "Clicker", click_function = "buttonClick_place", function_owner = self,
position = { 4.2, 1, 0 }, rotation = { 0, 0, 0 }, height = 500, width = 1100,
font_size = 350, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
})
self.createButton({
label = "Counter", click_function = "buttonClick_recall", function_owner = self,
position = { -4.2, 1, -0.1 }, rotation = { 0, 0, 0 }, height = 500, width = 1300,
font_size = 350, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
})
self.createButton({
label = "Add Draw 1 Buttons", click_function = "addDraw1Buttons", function_owner = self,
position = { 0, 1, -2.5 }, rotation = { 0, 0, 0 }, height = 500, width = 2600,
font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
})
--[[
self.createButton({
label="Setup", click_function="buttonClick_setup", function_owner=self,
position={-6,1,0}, rotation={0,90,0}, height=500, width=1200,
font_size=350, color={0,0,0}, font_color={1,1,1}
})
--]]
end
function addDraw1Buttons()
if ADD_BUTTONS_DISABLED then return end
local mats = { "8b081b", "bd0ff4", "383d8b", "0840d5" }
for i, guid in ipairs(mats) do
local mat = getObjectFromGUID(guid)
mat.createButton({
label = "Draw 1",
click_function = "doDrawOne",
function_owner = mat,
position = { 1.84, 0.1, -0.36 },
scale = { 0.12, 0.12, 0.12 },
width = 800,
height = 280,
font_size = 180
})
end
ADD_BUTTONS_DISABLED = true
end
--Sends objects from bag/table to their saved position/rotation
function buttonClick_place()
local bagObjList = self.getObjects()
for guid, entry in pairs(memoryList) do
local obj = getObjectFromGUID(guid)
--If obj is out on the table, move it to the saved pos/rot
if obj ~= nil then
obj.setPositionSmooth(entry.pos)
obj.setRotationSmooth(entry.rot)
obj.setLock(entry.lock)
else
--If obj is inside of the bag
for _, bagObj in ipairs(bagObjList) do
if bagObj.guid == guid then
local item = self.takeObject({
guid = guid, position = entry.pos, rotation = entry.rot,
})
item.setLock(entry.lock)
break
end
end
end
end
broadcastToAll("Objects Placed", { 1, 1, 1 })
end
--Recalls objects to bag from table
function buttonClick_recall()
for guid, entry in pairs(memoryList) do
local obj = getObjectFromGUID(guid)
if obj ~= nil then self.putObject(obj) end
end
broadcastToAll("Objects Recalled", { 1, 1, 1 })
end
--Utility functions
--Find delta (difference) between 2 x/y/z coordinates
function findOffsetDistance(p1, p2, obj)
local deltaPos = {}
local bounds = obj.getBounds()
deltaPos.x = (p2.x - p1.x)
deltaPos.y = (p2.y - p1.y) + (bounds.size.y - bounds.offset.y)
deltaPos.z = (p2.z - p1.z)
return deltaPos
end
--Used to rotate a set of coordinates by an angle
function rotateLocalCoordinates(desiredPos, obj)
local objPos, objRot = obj.getPosition(), obj.getRotation()
local angle = math.rad(objRot.y)
local x = desiredPos.x * math.cos(angle) - desiredPos.z * math.sin(angle)
local z = desiredPos.x * math.sin(angle) + desiredPos.z * math.cos(angle)
return { x = x, y = desiredPos.y, z = z }
end
--Coroutine delay, in seconds
function wait(time)
local start = os.time()
repeat coroutine.yield(0) until os.time() > start + time
end
--Duplicates a table (needed to prevent it making reference to the same objects)
function duplicateTable(oldTable)
local newTable = {}
for k, v in pairs(oldTable) do
newTable[k] = v
end
return newTable
end
--Moves scripted highlight from all objects
function removeAllHighlights()
for _, obj in ipairs(getAllObjects()) do
obj.highlightOff()
end
end
--Round number (num) to the Nth decimal (dec)
function round(num, dec)
local mult = 10 ^ (dec or 0)
return math.floor(num * mult + 0.5) / mult
end

View File

@ -17,7 +17,7 @@
<Button icon="yog-sothoth" tooltip="Extras" onClick="onClick_toggleUi(Extras)"/>
<Button icon="elder-sign" tooltip="Investigators" onClick="onClick_toggleUi(Investigators)"/>
<Button icon="devourer" tooltip="Community Content" onClick="onClick_toggleUi(Community Content)"/>
<Button icon="option-gear" tooltip="Options" onClick="onClick_toggleUi(Options)" active="false"/>
<Button icon="option-gear" tooltip="Options" onClick="onClick_toggleUi(Options)"/>
<!--<Button icon="download" tooltip="ArkhamDB Deck Importer" onClick="onClick_toggleUi(Deck Importer)"/> -->
</VerticalLayout>