diff --git a/src/arkhamdb/CommandManager.ttslua b/src/arkhamdb/CommandManager.ttslua deleted file mode 100644 index 3d4c4583..00000000 --- a/src/arkhamdb/CommandManager.ttslua +++ /dev/null @@ -1,287 +0,0 @@ ---- ---- Generated by EmmyLua(https://github.com/EmmyLua) ---- Created by Whimsical. ---- DateTime: 2021-08-22 6:36 a.m. ---- - ----@class CommandTableEntry ----@field public object TTSObject ----@field public runOn ArkhamImport_Command_RunDirectives -local CommandTableEntry = {} - ----@type table -local commands = {} - ----@type table -local found_commands = {} - ----@type table -local command_state - -local function load_commands() - local command_objects = getObjectsWithTag("import_command") - - for _, object in ipairs(command_objects) do - commands[object:getVar("command_name")] = { - object = object, - runOn = object:getTable("runOn") - } - end -end - ----@param configuration ArkhamImportConfiguration ----@param message string ----@return ArkhamImport_CommandManager_InitializationResults -local function build_error(configuration, message) - return { - configuration = configuration, - is_successful = false, - error_message = message - } -end - ----@param source table ----@param updates table -local function merge_tables(source, updates) - for key, _ in pairs(source) do - local update = updates[key] - if update~=nil then - source[key] = update - end - end -end - ----@param instruction TTSObject ----@param initialization_state any ----@param arguments string[] ----@return ArkhamImport_CommandManager_InitializationResults|nil -local function run_instruction(instruction, initialization_state, arguments) - ---@type ArkhamImport_Command_DescriptionInstructionResults - local result = instruction:call("do_instruction", { - configuration = initialization_state.configuration, - command_state = initialization_state.command_state, - arguments = arguments - }) - - if (not result) or type(result)~="table" then - return build_error(initialization_state.configuration, table.concat({"Command \"", instruction:getName(), "\" did not return a table from do_instruction call. Type \"", type(result), "\" was returned."})) - end - - if not result.is_successful then - return build_error(result.configuration, result.error_message) - end - - merge_tables(initialization_state, result) -end - ----@param description string ----@param initialization_state table ----@return ArkhamImport_CommandManager_InitializationResults|nil -local function initialize_instructions(description, initialization_state) - for _, instruction in ipairs(parse(description)) do - local command = commands[instruction.command] - - if command==nil then - return build_error(initialization_state.configuration, table.concat({ "Could not find command \"", command, "\"."})) - end - - found_commands[instruction.command] = true - - if command.runOn.instructions then - local error = run_instruction(command.object, initialization_state, instruction.arguments) - if error then return error end - end - end -end - ----@param parameters ArkhamImport_CommandManager_InitializationArguments ----@return table -local function create_initialize_state(parameters) - return { - configuration = parameters.configuration, - command_state = {} - } -end - ----@param parameters ArkhamImport_CommandManager_InitializationArguments ----@return ArkhamImport_CommandManager_InitializationResults -function initialize(parameters) - found_commands = {} - load_commands() - - local initialization_state = create_initialize_state(parameters) - - local error = initialize_instructions(parameters.description, initialization_state) - if error then return error end - - command_state = initialization_state.command_state - - return { - configuration = initialization_state.configuration, - is_successful = true - } -end - ----@param parameters ArkhamImport_CommandManager_HandlerArguments ----@return table -local function create_handler_state(parameters) - return { - card = parameters.card, - handled = false, - zone = parameters.zone, - command_state = command_state - }, - { - configuration = parameters.configuration, - source_guid = parameters.source_guid - } -end - ----@param card ArkhamImportCard ----@param zone = string[] ----@param handled boolean ----@param error_message string ----@return ArkhamImport_CommandManager_HandlerResults -local function create_handler_error(card, zone, handled, error_message) - return { - handled = handled, - card = card, - zone = zone, - is_successful = false, - error_message = error_message - } -end - ----@param handler TTSObject ----@param handler_state table ----@param handler_constants table ----@return ArkhamImport_CommandManager_HandlerResults|nil -local function call_handler(handler, handler_state, handler_constants) - ---@type ArkhamImport_CommandManager_HandlerResults - local results = handler:call("handle_card", { - configuration = handler_constants.configuration, - source_guid = handler_constants.source_guid, - card = handler_state.card, - zone = handler_state.zone, - command_state = handler_state.command_state, - }) - - if not results.is_successful then return create_handler_error(results.card, results.zone, results.handled, results.error_message) end - - merge_tables(handler_state, results) - command_state = handler_state.command_state -end - ----@param handler_state table ----@param handler_constants table ----@return ArkhamImport_CommandManager_HandlerResults|nil -local function run_handlers(handler_state, handler_constants) - for command_name, _ in pairs(found_commands) do - local command = commands[command_name] - if command.runOn.handlers then - local error = call_handler(command.object, handler_state, handler_constants) - if error then return error end - - if (handler_state.handled) then return end - end - end -end - ----@param parameters ArkhamImport_CommandManager_HandlerArguments ----@return ArkhamImport_CommandManager_HandlerResults -function handle(parameters) - local handler_state, handler_constants = create_handler_state(parameters) - - local error = run_handlers(handler_state, handler_constants) - if error then return error end - - return { - handled = handler_state.handled, - card = handler_state.card, - zone = handler_state.zone, - is_successful = true - } -end - ----@param description string ----@return ArkhamImportCommandParserResult[] -function parse(description) - local input = description - - if #input<=4 then return {} end - - ---@type string - local current, l1, l2, l3 = "", "", "", "" - - local concat = table.concat - - local function advance() - current, l1, l2, l3 = l1, l2, l3, input:sub(1,1) - input = input:sub(2) - end - - local function advance_all() - current, l1, l2, l3 = input:sub(1,1), input:sub(2,2), input:sub(3,3), input:sub(4,4) - input = input:sub(5) - end - - advance_all() - - ---@type ArkhamImportCommandParserResult[] - local results = {} - - ---@type string - local command - - ---@type string[] - local arguments = {} - - ---@type string - local separator - - ---@type string[] - local result = {} - - while #current>0 do - if current=="<" and l1=="?" and l2 == "?" then - command = nil - arguments = {} - separator = l3 - result = {} - - advance_all() - elseif current == "?" and l1 == "?" and l2 == ">" then - if not command then - table.insert(results, { - command = concat(result), - arguments = {} - }) - else - table.insert(arguments, concat(result)) - table.insert(results, { - command = command, - arguments = arguments - }) - end - - separator = nil - current, l1, l2, l3 = l3, input:sub(1,1), input:sub(2,2), input:sub(3,3) - input = input:sub(4) - elseif current == separator then - if not command then - command = concat(result) - else - table.insert(arguments, concat(result)) - end - result = {} - advance() - else - if separator~=nil then - table.insert(result, current) - end - advance() - end - end - - return results -end diff --git a/src/arkhamdb/DeckImporterMain.ttslua b/src/arkhamdb/DeckImporterMain.ttslua index df8fde31..9835c2ed 100644 --- a/src/arkhamdb/DeckImporterMain.ttslua +++ b/src/arkhamdb/DeckImporterMain.ttslua @@ -1,23 +1,43 @@ ---- ---- Generated by EmmyLua(https://github.com/EmmyLua) ---- Created by Whimsical. ---- DateTime: 2021-08-19 6:38 a.m. ---- +local Zones = require("playermat/Zones") +require("arkhamdb/LoaderUi") ----@type ArkhamImportConfiguration - -require("src/arkhamdb/LoaderUi") -local Zones = require("src/arkhamdb/Zones") +local bondedList = { } +local customizationRowsWithFields = { } +-- inputMap maps from (our 1-indexes) customization row index to inputValue table index +-- The Raven Quill +customizationRowsWithFields["09042"] = { } +customizationRowsWithFields["09042"].inputCount = 2 +customizationRowsWithFields["09042"].inputMap = { } +customizationRowsWithFields["09042"].inputMap[1] = 1 +customizationRowsWithFields["09042"].inputMap[5] = 2 +-- Friends in Low Places +customizationRowsWithFields["09060"] = { } +customizationRowsWithFields["09060"].inputCount = 2 +customizationRowsWithFields["09060"].inputMap = { } +customizationRowsWithFields["09060"].inputMap[1] = 1 +customizationRowsWithFields["09060"].inputMap[3] = 2 +-- Living Ink +customizationRowsWithFields["09079"] = { } +customizationRowsWithFields["09079"].inputCount = 3 +customizationRowsWithFields["09079"].inputMap = { } +customizationRowsWithFields["09079"].inputMap[1] = 1 +customizationRowsWithFields["09079"].inputMap[5] = 2 +customizationRowsWithFields["09079"].inputMap[6] = 3 +-- Grizzled +customizationRowsWithFields["09101"] = { } +customizationRowsWithFields["09101"].inputCount = 3 +customizationRowsWithFields["09101"].inputMap = { } +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 + ERROR = 0, + WARNING = 1, + INFO = 2, + DEBUG = 3 } ---@type fun(text: string) @@ -27,12 +47,12 @@ 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 + 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 @@ -46,12 +66,10 @@ local function debugPrint(message, priority, color) end end ----@param str string ----@return string local function fixUtf16String(str) - return str:gsub("\\u(%w%w%w%w)", function (match) - return string.char(tonumber(match,16)) - end) + return str:gsub("\\u(%w%w%w%w)", function(match) + return string.char(tonumber(match, 16)) + end) end --Forward declaration @@ -63,9 +81,9 @@ local tabooList = {} ---@return ArkhamImportConfiguration local function getConfiguration() - local configuration = getObjectsWithTag(tags.configuration)[1]:getTable("configuration") - printPriority = configuration.priority - return configuration + local configuration = getObjectsWithTag(tags.configuration)[1]:getTable("configuration") + printPriority = configuration.priority + return configuration end function onLoad(script_state) @@ -74,36 +92,33 @@ function onLoad(script_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 - local cards = {} + Request.start({ configuration.api_uri, configuration.taboo }, function(status) + local json = JSON.decode(fixUtf16String(status.text)) + for _, taboo in pairs(json) do + ---@type + 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 - } + for _, card in pairs(JSON.decode(taboo.cards)) do + cards[card.code] = true end - return true, nil - end) + + tabooList[taboo.id] = { + date = taboo.date_start, + cards = cards + } + end + return true, nil + end) end -function onSave() - return JSON.encode(getUiState()) -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 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 @@ -112,14 +127,14 @@ local function onDeckResult(deck, playerColor, configuration) return end - debugPrint(table.concat({ "Found decklist: ", deck.name}), Priority.INFO, playerColor) + 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, ": "}, Priority.DEBUG) + debugPrint(table.concat({ "-", deck.name, "-" }), Priority.DEBUG) + for k, v in pairs(deck) do + if type(v) == "table" then + debugPrint(table.concat { k, ":
" }, Priority.DEBUG) else - debugPrint(table.concat {k, ": ", tostring(v)}, Priority.DEBUG) + debugPrint(table.concat { k, ": ", tostring(v) }, Priority.DEBUG) end end debugPrint("", Priority.DEBUG) @@ -150,71 +165,78 @@ local function onDeckResult(deck, playerColor, configuration) return end - loadCards(slots, playerColor, commandManager, configuration, results.configuration) + -- 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, playerColor, commandManager, + configuration, results.configuration, customizations) 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 +-- 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 + if cardId == RANDOM_WEAKNESS_ID then hasRandomWeakness = true break end end - if (hasRandomWeakness) then + 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 the UI indicates that 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 +-- 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 + if getUiState().investigators then local investigatorId = deck.investigator_code - slots[investigatorId.."-m"] = 1 - local parallelFront = deck.meta ~= nil and deck.meta.alternate_front ~= nil and deck.meta.alternate_front ~= "" - local parallelBack = deck.meta ~= nil and deck.meta.alternate_back ~= nil and deck.meta.alternate_back ~= "" - if (parallelFront and parallelBack) then - investigatorId = investigatorId.."-p" - elseif (parallelFront) then - investigatorId = investigatorId.."-pf" - elseif (parallelBack) then - investigatorId = investigatorId.."-pb" + 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 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 +-- 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 = { } + 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 @@ -224,27 +246,24 @@ function extractBondedCards(slots, configuration) end end --- Check the deck for any 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 +-- 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 + if tabooId then local allCardsBag = getObjectFromGUID(configuration.card_bag_guid) for cardId, _ in pairs(tabooList[tabooId].cards) do - if (slots[cardId] ~= nil) then + 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 + -- 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 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) + debugPrint("Taboo version for " .. basicCard.data.Nickname .. " is not available. Using standard version", + Priority.WARNING, playerColor) else - slots[cardId.."-t"] = slots[cardId] + slots[cardId .. "-t"] = slots[cardId] slots[cardId] = nil end end @@ -252,38 +271,104 @@ function checkTaboos(tabooId, slots, playerColor, configuration) 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. +-- 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 method uses an encapsulated coroutine with yields to make the card spawning cleaner. -- --- 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 playerColor String Color name of the player mat to place this deck --- on (e.g. "Red") --- Param commandManager --- Param configuration: Loader configuration object --- Param command_config: -function loadCards(slots, playerColor, commandManager, configuration, command_config) +---@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 playerColor String Color name of the player mat to place this deck on (e.g. "Red") +---@param configuration: Loader configuration object +---@param customizations: ArkhamDB data for customizations on customizable cards +function loadCards(slots, investigatorId, playerColor, commandManager, configuration, command_config, customizations) function coinside() local allCardsBag = getObjectFromGUID(configuration.card_bag_guid) - local yPos = { } - local cardsToSpawn = { } + local yPos = {} + local cardsToSpawn = {} for cardId, cardCount in pairs(slots) do local card = allCardsBag.call("getCardById", { id = cardId }) - if (card ~= nil) then + if card ~= nil then local cardZone = Zones.getDefaultCardZone(card.metadata) for i = 1, cardCount do table.insert(cardsToSpawn, { data = card.data, metadata = card.metadata, zone = cardZone }) end + + -- upgrade sheets for customizable cards + local upgradesheet = allCardsBag.call("getCardById", { id = cardId .. "-c" }) + if upgradesheet ~= nil then + + -- update metadata for spawned upgrade sheets + local upgrades = customizations["cus_" .. cardId] + + if upgrades ~= nil then + -- initialize tables + -- markedBoxes: contains the amount of markedBoxes (left to right) per row (starting at row 1) + -- inputValues: contains the amount of inputValues per row (starting at row 0) + local markedBoxes = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } + local inputValues = {} + local index_xp = {} + + -- get the index and xp values (looks like this: X|X,X|X, ..) + for str in string.gmatch(customizations["cus_" .. cardId], "([^,]+)") do + table.insert(index_xp, str) + end + + -- split each pair and assign it to the proper position in markedBoxes + if (customizationRowsWithFields[cardId] ~= nil) then + for i = 1, customizationRowsWithFields[cardId].inputCount do + table.insert(inputValues, "") + end + end + local inputCount = 0 + for _, entry in ipairs(index_xp) do + local counter = 0 + local index = 0 + + -- if found number is 0, then only get inputvalue + for str in string.gmatch(entry, "([^|]+)") do + counter = counter + 1 + if counter == 1 then + index = tonumber(str) + 1 + elseif counter == 2 then + markedBoxes[index] = tonumber(str) + elseif counter == 3 and str ~= "" then + if (cardId == "09042") then + inputValues[customizationRowsWithFields[cardId].inputMap[index]] = + convertRavenQuillSelections(str) + else + inputValues[customizationRowsWithFields[cardId].inputMap[index]] = str + end + end + end + end + + -- remove first entry in markedBoxes if row 0 has textbox + if customizationRowsWithFields[cardId] ~= nil + and customizationRowsWithFields[cardId].inputCount > 0 then + table.remove(markedBoxes, 1) + end + + -- write the loaded values to the save_data of the sheets + upgradesheet.data["LuaScriptState"] = JSON.encode({ markedBoxes, inputValues }) + table.insert(cardsToSpawn, { data = upgradesheet.data, metadata = upgradesheet.metadata, zone = "SetAside4" }) + end + end + + -- spawn additional minicard for 'Summoned Servitor' + if cardId == "09080" then + local servitor = allCardsBag.call("getCardById", { id = "09080-m" }) + table.insert(cardsToSpawn, { data = servitor.data, metadata = servitor.metadata, zone = "SetAside6" }) + end + slots[cardId] = 0 end end -- TODO: Re-enable this later, as a command - --handleAltInvestigatorCard(cardsToSpawn, "promo", configuration) + -- handleAltInvestigatorCard(cardsToSpawn, "promo", configuration) table.sort(cardsToSpawn, cardComparator) @@ -292,21 +377,21 @@ function loadCards(slots, playerColor, commandManager, configuration, command_co -- These should probably be commands, once the command handler is updated handleStartsInPlay(cardsToSpawn) handleAncestralKnowledge(cardsToSpawn) + handleUnderworldMarket(cardsToSpawn, playerColor) + handleHunchDeck(investigatorId, cardsToSpawn, playerColor) -- Count the number of cards in each zone so we know if it's a deck or card. - -- TTS's Card vs. Deck distinction requires this since we can't spawn a deck - -- with only one card + -- TTS's Card vs. Deck distinction requires this since we can't spawn a deck with only one card local zoneCounts = getZoneCounts(cardsToSpawn) - local zoneDecks = { } + local zoneDecks = {} for zone, count in pairs(zoneCounts) do - if (count > 1) then + if count > 1 then zoneDecks[zone] = buildDeckDataTemplate() end end - -- For each card in a deck zone, add it to that deck. Otherwise, spawn it - -- directly + -- For each card in a deck zone, add it to that deck. Otherwise, spawn it directly for _, spawnCard in ipairs(cardsToSpawn) do - if (zoneDecks[spawnCard.zone] ~= nil) then + if zoneDecks[spawnCard.zone] ~= nil then addCardToDeck(zoneDecks[spawnCard.zone], spawnCard.data) else local cardPos = Zones.getZonePosition(playerColor, spawnCard.zone) @@ -314,41 +399,56 @@ function loadCards(slots, playerColor, commandManager, configuration, command_co spawnObjectData({ data = spawnCard.data, position = cardPos, - rotation = Zones.getDefaultCardRotation(playerColor, spawnCard.zone)}) + rotation = Zones.getDefaultCardRotation(playerColor, spawnCard.zone), + }) end end -- Spawn each of the decks for zone, deck in pairs(zoneDecks) do local deckPos = Zones.getZonePosition(playerColor, zone) deckPos.y = 3 + local spreadCallback = nil; + if (zone == "SetAside4") then + -- SetAside4 is reserved for customization cards, and we want them spread on the table + -- so their checkboxes are visible + if (playerColor == "White") then + deckPos.z = deckPos.z + (#deck.ContainedObjects - 1) + elseif (playerColor == "Green") then + deckPos.x = deckPos.x + (#deck.ContainedObjects - 1) + end + spreadCallback = function(deck) deck.spread(1.0) end + end spawnObjectData({ data = deck, position = deckPos, - rotation = Zones.getDefaultCardRotation(playerColor, zone)}) + rotation = Zones.getDefaultCardRotation(playerColor, zone), + callback_function = spreadCallback + }) coroutine.yield(0) end -- Look for any cards which haven't been loaded local hadError = false for cardId, remainingCount in pairs(slots) do - if (remainingCount > 0) then + 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) + 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 - 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) end end if (not hadError) then @@ -356,9 +456,38 @@ function loadCards(slots, playerColor, commandManager, configuration, command_co end return 1 end + startLuaCoroutine(self, "coinside") end +-- Conver the Raven Quill's selections from card IDs to card names. This could be more elegant +-- but the inputs are very static so we're using some brute force. +-- @param An ArkhamDB string indicating the customization selections for The Raven's Quill. Should +-- be either a single card ID or two separated by a ^ (e.g. XXXXX^YYYYY) +function convertRavenQuillSelections(selectionString) + if (string.len(selectionString) == 5) then + return getCardName(selectionString) + elseif (string.len(selectionString) == 11) then + return getCardName(string.sub(selectionString, 1, 5))..", "..getCardName(string.sub(selectionString, 7)) + end +end + +-- Returns the simple name of a card given its ID. This will find the card and strip any trailing +-- SCED-specific suffixes such as (Taboo) or (Level) +function getCardName(cardId) + local configuration = getConfiguration() + local allCardsBag = getObjectFromGUID(configuration.card_bag_guid) + local card = allCardsBag.call("getCardById", { id = cardId }) + if (card ~= nil) then + local name = card.data.Nickname + if (string.find(name, " %(")) then + return string.sub(name, 1, string.find(name, " %(") - 1) + else + return name + end + end +end + -- Inserts a card into the given deck. This does three things: -- 1. Add the card's data to ContainedObjects -- 2. Add the card's ID (the TTS CardID, not the Arkham ID) to the deck's @@ -367,23 +496,55 @@ end -- 3. Extract the card's CustomDeck table and add it to the deck. The deck's -- "CustomDeck" field is a list of all CustomDecks used by cards within the -- deck, keyed by the DeckID and referencing the custom deck table --- Param deck: TTS deck data structure to add to --- Param card: Data for the card to be inserted +---@param deck: TTS deck data structure to add to +---@param card: Data for the card to be inserted function addCardToDeck(deck, cardData) + for customDeckId, customDeckData in pairs(cardData.CustomDeck) do + if (deck.CustomDeck[customDeckId] == nil) then + -- CustomDeck not added to deck yet, add it + deck.CustomDeck[customDeckId] = customDeckData + elseif (deck.CustomDeck[customDeckId].FaceURL == customDeckData.FaceURL) then + -- CustomDeck for this card matches the current one for the deck, do nothing + else + -- CustomDeck data conflict + local newDeckId = nil + for deckId, customDeck in pairs(deck.CustomDeck) do + if (customDeckData.FaceURL == customDeck.FaceURL) then + newDeckId = deckId + end + end + if (newDeckId == nil) then + -- No non-conflicting custom deck for this card, add a new one + newDeckId = findNextAvailableId(deck.CustomDeck, "1000") + deck.CustomDeck[newDeckId] = customDeckData + end + -- Update the card with the new CustomDeck info + cardData.CardID = newDeckId..string.sub(cardData.CardID, 5) + cardData.CustomDeck[customDeckId] = nil + cardData.CustomDeck[newDeckId] = customDeckData + break + end + end table.insert(deck.ContainedObjects, cardData) table.insert(deck.DeckIDs, cardData.CardID) - for customDeckId, customDeckData in pairs(cardData.CustomDeck) do - deck.CustomDeck[customDeckId] = customDeckData +end + +function findNextAvailableId(objectTable, startId) + local id = startId + while (objectTable[id] ~= nil) do + id = tostring(tonumber(id) + 1) end + + return id end -- Count the number of cards in each zone --- Param cards: Table of {cardData, cardMetadata, zone} --- Return: Table of {zoneName=zoneCount} +---@param cards: Table of {cardData, cardMetadata, zone} +---@return: Table of {zoneName=zoneCount} function getZoneCounts(cards) - local counts = { } + local counts = {} for _, card in ipairs(cards) do - if (counts[card.zone] == nil) then + if counts[card.zone] == nil then counts[card.zone] = 1 else counts[card.zone] = counts[card.zone] + 1 @@ -393,37 +554,36 @@ function getZoneCounts(cards) return counts end --- Create an empty deck data table which can have cards added to it. This +-- Create an empty deck data table which can have cards added to it. This -- creates a new table on each call without using metatables or previous -- definitions because we can't be sure that TTS doesn't modify the structure --- Return: Table containing the minimal TTS deck data structure +---@return: Table containing the minimal TTS deck data structure function buildDeckDataTemplate() - local deck = { } + local deck = {} deck.Name = "Deck" -- Card data. DeckIDs and CustomDeck entries will be built from the cards - deck.ContainedObjects = { } - deck.DeckIDs = { } - deck.CustomDeck = { } + deck.ContainedObjects = {} + deck.DeckIDs = {} + deck.CustomDeck = {} - -- Transform is required, Position and Rotation will be overridden by the - -- spawn call so can be omitted here + -- Transform is required, Position and Rotation will be overridden by the spawn call so can be omitted here deck.Transform = { scaleX = 1, scaleY = 1, - scaleZ = 1, } + scaleZ = 1, + } return deck end --- Get the PBN (Permanent/Bonded/Normal) value from the given metadata. --- Return: 1 for Permanent, 2 for Bonded, or 3 for Normal. The actual values --- are irrelevant as they provide only grouping and the order between them --- doesn't matter. -function getPbn(metadata) - if (metadata.permanent) then +-- Get the PBCN (Permanent/Bonded/Customizable/Normal) value from the given metadata. +---@return: 1 for Permanent, 2 for Bonded or 4 for Normal. The actual values are +-- irrelevant as they provide only grouping and the order between them doesn't matter. +function getpbcn(metadata) + if metadata.permanent then return 1 - elseif (metadata.bonded_to ~= nil) then + elseif metadata.bonded_to ~= nil then return 2 else -- Normal card return 3 @@ -432,26 +592,26 @@ end -- Comparison function used to sort the cards in a deck. Groups bonded or -- permanent cards first, then sorts within theose types by name/subname. --- Normal cards will sort in standard alphabetical order, while permanent/bonded --- will be in reverse alphabetical order. +-- Normal cards will sort in standard alphabetical order, while +-- permanent/bonded/customizable will be in reverse alphabetical order. -- -- Since cards spawn in the order provided by this comparator, with the first -- cards ending up at the bottom of a pile, this ordering will spawn in reverse -- alphabetical order. This presents the cards in order for non-face-down -- areas, and presents them in order when Searching the face-down deck. function cardComparator(card1, card2) - local pbn1 = getPbn(card1.metadata) - local pbn2 = getPbn(card2.metadata) - if (pbn1 ~= pbn2) then - return pbn1 > pbn2 + local pbcn1 = getpbcn(card1.metadata) + local pbcn2 = getpbcn(card2.metadata) + if pbcn1 ~= pbcn2 then + return pbcn1 > pbcn2 end - if (pbn1 == 3) then - if (card1.data.Nickname ~= card2.data.Nickname) then + if pbcn1 == 3 then + if card1.data.Nickname ~= card2.data.Nickname then return card1.data.Nickname < card2.data.Nickname end return card1.data.Description < card2.data.Description else - if (card1.data.Nickname ~= card2.data.Nickname) then + if card1.data.Nickname ~= card2.data.Nickname then return card1.data.Nickname > card2.data.Nickname end return card1.data.Description > card2.data.Description @@ -463,25 +623,24 @@ end -- --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 +---@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 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 + 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 + local altMinicard = allCardsBag.call("getCardById", { id = investigatorId .. "-" .. altVersionTag .. "-m" }) + if altMinicard ~= nil then card.data = altMinicard.data card.metadata = altMinicard.metadata end @@ -492,24 +651,17 @@ end -- Place cards which start in play (Duke, Sophie) in the play area function handleStartsInPlay(cardList) for _, card in ipairs(cardList) do - -- 02014 = Duke (Ashcan Pete) - -- 03009 = Sophie (Mark Harrigan) - if (card.metadata.id == "02014" or card.metadata.id == "03009") then - card.zone = "BlankTop" - end + if card.metadata.startsInPlay then card.zone = "BlankTop" end end end --- Check to see if the deck list has Ancestral Knowledge. If it does, move 5 --- random skills to SetAside3 +-- Check to see if the deck list has Ancestral Knowledge. If it does, move 5 random skills to SetAside3 function handleAncestralKnowledge(cardList) local hasAncestralKnowledge = false - local skillList = { } - -- Have to process the entire list to check for Ancestral Knowledge and get - -- all possible skills, so do both in one pass + local skillList = {} + -- Have to process the entire list to check for Ancestral Knowledge and get all possible skills, so do both in one pass for i, card in ipairs(cardList) do - if (card.metadata.id == "07303") then - -- Ancestral Knowledge found + if card.metadata.id == "07303" then hasAncestralKnowledge = true card.zone = "SetAside3" elseif (card.metadata.type == "Skill" @@ -518,8 +670,8 @@ function handleAncestralKnowledge(cardList) table.insert(skillList, i) end end - if (hasAncestralKnowledge) then - for i = 1,5 do + if hasAncestralKnowledge then + for i = 1, 5 do -- Move 5 random skills to SetAside3 local skillListIndex = math.random(#skillList) cardList[skillList[skillListIndex]].zone = "UnderSetAside3" @@ -528,31 +680,112 @@ function handleAncestralKnowledge(cardList) end end --- Test method. Loads all decks which were submitted to ArkhamDB on a given --- date window. +-- 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 +function handleUnderworldMarket(cardList, playerColor) + local hasMarket = false + local illicitList = {} + -- Process the entire list to check for Underworld Market and get all possible skills, doing both in one pass + for i, card in ipairs(cardList) do + if card.metadata.id == "09077" then + -- Underworld Market found + hasMarket = true + card.zone = "SetAside3" + elseif (string.find(card.metadata.traits, "Illicit", 1, true) + and card.metadata.bonded_to == nil + and not card.metadata.weakness) then + table.insert(illicitList, i) + end + end + + 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) + 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) + -- Performance here may be an issue, as table.remove() is an O(n) operation + -- which makes the full shift O(n^2). But keep it simple unless it becomes + -- a problem + for i = #illicitList, 1, -1 do + local moving = cardList[illicitList[i]] + moving.zone = "UnderSetAside3" + table.remove(cardList, illicitList[i]) + table.insert(cardList, moving) + end + + if #illicitList > 10 then + debugPrint("Moved all " .. #illicitList .. " Illicit cards to the Market deck, reduce it to 10", Priority.INFO, + playerColor) + else + debugPrint("Built the Market deck", Priority.INFO, 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 +function handleHunchDeck(investigatorId, cardList, playerColor) + if investigatorId == "05002" then -- Joe Diamond + local insightList = {} + for i, card in ipairs(cardList) do + if (card.metadata.type == "Event" + and string.match(card.metadata.traits, "Insight") + and card.metadata.bonded_to == nil) then + table.insert(insightList, i) + end + end + -- Process insights to move them to the hunch deck. This is done in reverse + -- order because the sorting needs to be reversed (deck sorts for face down) + -- Performance here may be an issue, as table.remove() is an O(n) operation + -- which makes the full shift O(n^2). But keep it simple unless it becomes + -- a problem + for i = #insightList, 1, -1 do + local moving = cardList[insightList[i]] + moving.zone = "SetAside5" + table.remove(cardList, insightList[i]) + 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) + elseif #insightList > 11 then + debugPrint("Moved all " .. #insightList .. " Insight events to the hunch deck, reduce it to 11.", Priority.INFO, + playerColor) + else + debugPrint("Built Joe's hunch deck", Priority.INFO, playerColor) + end + 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 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) + 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 +-- 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" @@ -567,104 +800,104 @@ 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 +---@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. + -- 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"}) + 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 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, "") 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) + local deck = Request.start(deckUri, function(status) + if string.find(status.text, "") 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 + 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) + return true, JSON.decode(status.text) + end) deck:with(onDeckResult, playerColor, configuration) end ---@type Request Request = { - is_done = false, - is_successful = false + 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. +-- 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 = {} + local this = {} - setmetatable(this, self) - self.__index = self + setmetatable(this, self) + self.__index = self - if type(uri)=="table" then - uri = table.concat(uri, "/") - end + if type(uri) == "table" then + uri = table.concat(uri, "/") + end - this.uri = uri + this.uri = uri - WebRequest.get(uri, function(status) - configure(this, status) - end) + WebRequest.get(uri, function(status) + configure(this, status) + end) - return this + 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) +-- 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) + 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. +-- 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)) + 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[] @@ -672,47 +905,47 @@ end ---@param on_error fun(requests: Request[], vararg any[])|nil ---@vararg any function Request.with_all(requests, on_success, on_error, ...) - local parameters = table.pack(...) + local parameters = table.pack(...) - Wait.condition(function () - ---@type any[] - local results = {} + Wait.condition(function() + ---@type any[] + local results = {} - ---@type Request[] - local errors = {} + ---@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 + 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) + 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) + 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 diff --git a/src/arkhamdb/LoaderUi.ttslua b/src/arkhamdb/LoaderUi.ttslua index e8b16937..3ea446a4 100644 --- a/src/arkhamdb/LoaderUi.ttslua +++ b/src/arkhamdb/LoaderUi.ttslua @@ -1,15 +1,16 @@ local INPUT_FIELD_HEIGHT = 340 local INPUT_FIELD_WIDTH = 1500 +local FIELD_COLOR = { 0.9, 0.7, 0.5 } -local FIELD_COLOR = {0.9,0.7,0.5} - -local PRIVATE_TOGGLE_LABELS = { } +local PRIVATE_TOGGLE_LABELS = {} PRIVATE_TOGGLE_LABELS[true] = "Private" PRIVATE_TOGGLE_LABELS[false] = "Published" -local UPGRADED_TOGGLE_LABELS = { } + +local UPGRADED_TOGGLE_LABELS = {} UPGRADED_TOGGLE_LABELS[true] = "Upgraded" UPGRADED_TOGGLE_LABELS[false] = "Specific" -local LOAD_INVESTIGATOR_TOGGLE_LABELS = { } + +local LOAD_INVESTIGATOR_TOGGLE_LABELS = {} LOAD_INVESTIGATOR_TOGGLE_LABELS[true] = "Yes" LOAD_INVESTIGATOR_TOGGLE_LABELS[false] = "No" @@ -17,14 +18,13 @@ local redDeckId = "" local orangeDeckId = "" local whiteDeckId = "" local greenDeckId = "" + local privateDeck = true local loadNewestDeck = true local loadInvestigators = false -local loadingColor = "" --- Returns a table with the full state of the UI, including options and deck --- IDs. This can be used to persist via onSave(), or provide values for a load --- operation +-- Returns a table with the full state of the UI, including options and deck IDs. +-- This can be used to persist via onSave(), or provide values for a load operation -- Table values: -- redDeck: Deck ID to load for the red player -- orangeDeck: Deck ID to load for the orange player @@ -41,14 +41,13 @@ function getUiState() greenDeck = greenDeckId, private = privateDeck, loadNewest = loadNewestDeck, - investigators = loadInvestigators, + investigators = loadInvestigators } end --- Sets up the UI for the deck loader, populating fields from the given save --- state table decoded from onLoad() +-- Sets up the UI for the deck loader, populating fields from the given save state table decoded from onLoad() function initializeUi(savedUiState) - if (savedUiState ~= nil) then + if savedUiState ~= nil then redDeckId = savedUiState.redDeck orangeDeckId = savedUiState.orangeDeck whiteDeckId = savedUiState.whiteDeck @@ -56,14 +55,6 @@ function initializeUi(savedUiState) privateDeck = savedUiState.private loadNewestDeck = savedUiState.loadNewest loadInvestigators = savedUiState.investigators - else - redDeckId = "" - orangeDeckId = "" - whiteDeckId = "" - greenDeckId = "" - privateDeck = true - loadNewestDeck = true - loadInvestigators = true end makeOptionToggles() @@ -72,59 +63,35 @@ function initializeUi(savedUiState) end function makeOptionToggles() - -- Creates the three option toggle buttons. Each toggle assumes its index as - -- part of the toggle logic. IF YOU CHANGE THE ORDER OF THESE FIELDS YOU MUST - -- CHANGE THE EVENT HANDLERS - makePublicPrivateToggle() - makeLoadUpgradedToggle() - makeLoadInvestigatorsToggle() -end - -function makePublicPrivateToggle() + -- common parameters local checkbox_parameters = {} + checkbox_parameters.function_owner = self + checkbox_parameters.width = INPUT_FIELD_WIDTH + checkbox_parameters.height = INPUT_FIELD_HEIGHT + checkbox_parameters.scale = { 0.1, 0.1, 0.1 } + checkbox_parameters.font_size = 240 + checkbox_parameters.hover_color = { 0.4, 0.6, 0.8 } + checkbox_parameters.color = FIELD_COLOR + + -- public / private deck checkbox_parameters.click_function = "publicPrivateChanged" - checkbox_parameters.function_owner = self - checkbox_parameters.position = {0.25,0.1,-0.102} - checkbox_parameters.width = INPUT_FIELD_WIDTH - checkbox_parameters.height = INPUT_FIELD_HEIGHT - checkbox_parameters.tooltip = "Published or private deck.\n\n*****PLEASE USE A PRIVATE DECK IF JUST FOR TTS TO AVOID FLOODING ARKHAMDB PUBLISHED DECK LISTS!" + checkbox_parameters.position = { 0.25, 0.1, -0.102 } + checkbox_parameters.tooltip = "Published or private deck?\n\nPLEASE USE A PRIVATE DECK IF JUST FOR TTS TO AVOID FLOODING ARKHAMDB PUBLISHED DECK LISTS!" checkbox_parameters.label = PRIVATE_TOGGLE_LABELS[privateDeck] - checkbox_parameters.font_size = 240 - checkbox_parameters.scale = {0.1,0.1,0.1} - checkbox_parameters.color = FIELD_COLOR - checkbox_parameters.hover_color = {0.4,0.6,0.8} self.createButton(checkbox_parameters) -end -function makeLoadUpgradedToggle() - local checkbox_parameters = {} + -- load upgraded? checkbox_parameters.click_function = "loadUpgradedChanged" - checkbox_parameters.function_owner = self - checkbox_parameters.position = {0.25,0.1,-0.01} - checkbox_parameters.width = INPUT_FIELD_WIDTH - checkbox_parameters.height = INPUT_FIELD_HEIGHT - checkbox_parameters.tooltip = "Load newest upgrade, or exact deck" + checkbox_parameters.position = { 0.25, 0.1, -0.01 } + checkbox_parameters.tooltip = "Load newest upgrade or exact deck?" checkbox_parameters.label = UPGRADED_TOGGLE_LABELS[loadNewestDeck] - checkbox_parameters.font_size = 240 - checkbox_parameters.scale = {0.1,0.1,0.1} - checkbox_parameters.color = FIELD_COLOR - checkbox_parameters.hover_color = {0.4,0.6,0.8} self.createButton(checkbox_parameters) -end -function makeLoadInvestigatorsToggle() - local checkbox_parameters = {} + -- load investigators? checkbox_parameters.click_function = "loadInvestigatorsChanged" - checkbox_parameters.function_owner = self - checkbox_parameters.position = {0.25,0.1,0.081} - checkbox_parameters.width = INPUT_FIELD_WIDTH - checkbox_parameters.height = INPUT_FIELD_HEIGHT + checkbox_parameters.position = { 0.25, 0.1, 0.081 } checkbox_parameters.tooltip = "Spawn investigator cards?" checkbox_parameters.label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators] - checkbox_parameters.font_size = 240 - checkbox_parameters.scale = {0.1,0.1,0.1} - checkbox_parameters.color = FIELD_COLOR - checkbox_parameters.hover_color = {0.4,0.6,0.8} self.createButton(checkbox_parameters) end @@ -133,103 +100,74 @@ function makeDeckIdFields() local input_parameters = {} -- Parameters common to all entry fields input_parameters.function_owner = self - input_parameters.scale = {0.1,0.1,0.1} + input_parameters.scale = { 0.1, 0.1, 0.1 } input_parameters.width = INPUT_FIELD_WIDTH input_parameters.height = INPUT_FIELD_HEIGHT input_parameters.font_size = 320 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'" input_parameters.alignment = 3 -- Center input_parameters.color = FIELD_COLOR - input_parameters.font_color = {0, 0, 0} + input_parameters.font_color = { 0, 0, 0 } input_parameters.validation = 2 -- Integer -- Green input_parameters.input_function = "greenDeckChanged" - input_parameters.position = {-0.166,0.1,0.385} - input_parameters.value=greenDeckId + input_parameters.position = { -0.166, 0.1, 0.385 } + input_parameters.value = greenDeckId self.createInput(input_parameters) -- Red input_parameters.input_function = "redDeckChanged" - input_parameters.position = {0.171,0.1,0.385} - input_parameters.value=redDeckId + input_parameters.position = { 0.171, 0.1, 0.385 } + input_parameters.value = redDeckId self.createInput(input_parameters) -- White input_parameters.input_function = "whiteDeckChanged" - input_parameters.position = {-0.166,0.1,0.474} - input_parameters.value=whiteDeckId + input_parameters.position = { -0.166, 0.1, 0.474 } + input_parameters.value = whiteDeckId self.createInput(input_parameters) -- Orange input_parameters.input_function = "orangeDeckChanged" - input_parameters.position = {0.171,0.1,0.474} - input_parameters.value=orangeDeckId + input_parameters.position = { 0.171, 0.1, 0.474 } + input_parameters.value = orangeDeckId self.createInput(input_parameters) end --- Create the Build All button. This is a transparent button which covers the --- Build All portion of the background graphic +-- Create the Build All button. This is a transparent button which covers the Build All portion of the background graphic function makeBuildButton() local button_parameters = {} button_parameters.click_function = "loadDecks" button_parameters.function_owner = self - button_parameters.position = {0,0.1,0.71} + button_parameters.position = { 0, 0.1, 0.71 } button_parameters.width = 320 button_parameters.height = 30 - button_parameters.color = {0, 0, 0, 0} + button_parameters.color = { 0, 0, 0, 0 } button_parameters.tooltip = "Click to build all four decks!" self.createButton(button_parameters) end --- Event handler for the Public/Private toggle. Changes the local value and the --- labels to toggle the button +-- Event handlers for deck ID change +function redDeckChanged(_, _, inputValue) redDeckId = inputValue end + +function orangeDeckChanged(_, _, inputValue) orangeDeckId = inputValue end + +function whiteDeckChanged(_, _, inputValue) whiteDeckId = inputValue end + +function greenDeckChanged(_, _, inputValue) greenDeckId = inputValue end + +-- Event handlers for toggle buttons function publicPrivateChanged() - -- editButton uses parameters.index which is 0-indexed privateDeck = not privateDeck - self.editButton { - index = 0, - label = PRIVATE_TOGGLE_LABELS[privateDeck], - } + self.editButton { index = 0, label = PRIVATE_TOGGLE_LABELS[privateDeck] } end --- Event handler for the Upgraded toggle. Changes the local value and the --- labels to toggle the button function loadUpgradedChanged() - -- editButton uses parameters.index which is 0-indexed loadNewestDeck = not loadNewestDeck - self.editButton { - index = 1, - label = UPGRADED_TOGGLE_LABELS[loadNewestDeck], - } + self.editButton { index = 1, label = UPGRADED_TOGGLE_LABELS[loadNewestDeck] } end --- Event handler for the load investigator cards toggle. Changes the local --- value and the labels to toggle the button function loadInvestigatorsChanged() - -- editButton uses parameters.index which is 0-indexed loadInvestigators = not loadInvestigators - self.editButton { - index = 2, - label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators], - } -end - --- Event handler for deck ID change -function redDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected) - redDeckId = inputValue -end - --- Event handler for deck ID change -function orangeDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected) - orangeDeckId = inputValue -end - --- Event handler for deck ID change -function whiteDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected) - whiteDeckId = inputValue -end - --- Event handler for deck ID change -function greenDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected) - greenDeckId = inputValue + self.editButton { index = 2, label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators] } end function loadDecks() diff --git a/src/arkhamdb/MainLogic.ttslua b/src/arkhamdb/MainLogic.ttslua deleted file mode 100644 index 22f4750f..00000000 --- a/src/arkhamdb/MainLogic.ttslua +++ /dev/null @@ -1,542 +0,0 @@ ---- ---- Generated by EmmyLua(https://github.com/EmmyLua) ---- Created by Whimsical. ---- DateTime: 2021-08-19 6:38 a.m. ---- - ----@type ArkhamImportConfiguration - -local tags = { configuration = "import_configuration_provider" } - -local Priority = { - ERROR = 0, - WARNING = 1, - INFO = 2, - DEBUG = 3 -} - ----@type fun(text: string) -local print_fun = print -local print_priority = Priority.DEBUG - ----@param priority number ----@return string -function Priority.get_label(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 debug_print(message, priority) - if (print_priority >= priority) then - print_fun("[" .. Priority.get_label(priority) .. "] " .. message) - end -end - ----@param str string ----@return string -local function fix_utf16_string(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 -local taboo_list = {} - ----@type number -local deck_type_button_index - -local is_private_deck = true - -function on_decktype_checkbox_clicked() - self:editButton { - label = is_private_deck and "Published" or "Private", - index = deck_type_button_index - } - is_private_deck = not is_private_deck -end - ----@return ArkhamImportConfiguration -local function get_configuration() - local configuration = getObjectsWithTag(tags.configuration)[1]:getTable("configuration") - print_priority = configuration.priority - return configuration -end - ----@param configuration ArkhamImportConfiguration -local function initialize(_, configuration) - local builder = getObjectFromGUID(configuration.ui_builder_guid) - - deck_type_button_index = builder:call("create_ui", { - target_guid = self:getGUID(), - debug_deck_id = configuration.debug_deck_id, - checkbox_toggle_callback_name = "on_decktype_checkbox_clicked", - build_deck_callback_name = "build_deck" - }) -end - -function onLoad() - Wait.frames(function () - local configuration = get_configuration() - local taboo = Request.start({configuration.api_uri, configuration.taboo}, function (status) - local json = JSON.decode(fix_utf16_string(status.text)) - for _, taboo in pairs(json) do - ---@type - local cards = {} - - for _, card in pairs(JSON.decode(taboo.cards)) do - cards[card.code] = true - end - - taboo_list[taboo.id] = { - date = taboo.date_start, - cards = cards - } - end - return true, nil - end) - - taboo:with(initialize, configuration) - end, 1) -end - ----@param status WebRequestStatus ----@param number number ----@param is_bonded boolean ----@return boolean, ArkhamImportCard -local function on_card_request(status, number, is_bonded) - local text = fix_utf16_string(status.text) - - ---@type ArkhamImportCard - local card = JSON.decode(text) - card.count = number - card.is_bonded = is_bonded - - return true, card -end - ----@param configuration ArkhamImportConfiguration ----@param card_code string ----@param count number ----@param is_bonded boolean ----@return Request -local function add_card(configuration, card_code, count, is_bonded) - local api, card_path = configuration.api_uri, configuration.cards - local request = Request.start({api, card_path, card_code}, on_card_request, nil, count, is_bonded) - return request -end - ----@param source TTSObject ----@param count number ----@param zones ArkhamImportZone[] ----@param keep_card boolean ----@return fun(card: TTSObject) -local function position_card(source, count, zones, keep_card) - ---@param card TTSObject - return function (card) - - for n = 1, count do - local zone = zones[n] - - local destination = zone.is_absolute and zone.position or self:positionToWorld(zone.position) - local rotation = self:getRotation() + Vector(0, 0, zone.is_facedown and 180 or 0) - card:clone { - position = destination, - rotation = rotation - } - end - - if keep_card then source:putObject(card) else card:destruct() end - end -end - ----@param source TTSObject ----@param target_name string ----@param target_subname string ----@param count number ----@param zone ArkhamImportZone[] -local function process_card(source, target_name, target_subname, count, zone) - for _, card in ipairs(source:getObjects()) do - if (card.name == target_name and (not target_subname or card.description==target_subname)) then - source:takeObject { - position = {0, 1.5, 0}, - index = card.index, - smooth = false, - callback_function = position_card(source, count, zone, true) - } - debug_print(table.concat({ "Added", count, "of", target_name}, " "), Priority.DEBUG) - return - end - end - debug_print(table.concat({ "Card not found:", target_name}, " "), Priority.WARNING) -end - ----@param source TTSObject ----@param zones ArkhamImportZone[] -local function random_weakness(source, zones) - source:shuffle() - - local card = source:takeObject { - position = {0, 1.5, 0}, - index = 0, - smooth = false, - callback_function = position_card(source, 1, zones, false), - } - - broadcastToAll("Drew random basic weakness: " .. card:getName()) -end - ----@param configuration ArkhamImportConfiguration ----@param card_id string ----@param used_bindings table ----@param requests Request[] -local function process_bindings(configuration, card_id, used_bindings, requests) - local bondedCards = configuration.bonded_cards[card_id] - - if not bondedCards then return end - - if bondedCards.code then bondedCards = {bondedCards} end - - for _, bond in ipairs(bondedCards) do - if not used_bindings[bond.code] then - used_bindings[bond.code] = true - local result = add_card(configuration, bond.code, bond.count, true) - - table.insert(requests, result) - end - end -end - ----@param configuration ArkhamImportConfiguration ----@param slots table ----@return Request[] -local function load_cards(configuration, slots) - ---@type Request[] - local requests = {} - - ---@type - local used_bindings = {} -- Bonded cards that we've already processed - for card_id, number in pairs(slots) do - table.insert(requests, add_card(configuration, card_id, number, false)) - - process_bindings(configuration, card_id, used_bindings, requests) - end - - return requests -end - ----@type string[] -local parallel_component = {"", " (Parallel Back)", " (Parallel Front)", " (Parallel)"} - ----@param discriminators table ----@param card ArkhamImportCard ----@param taboo ArkhamImportTaboo ----@param meta table ----@return string, string|nil -local function get_card_selector(discriminators, card, taboo, meta) - local discriminator = discriminators[card.code] - - if card.type_code == "investigator" then - local parallel = (meta.alternate_front and 2 or 0) + (meta.alternate_back and 1 or 0) - - return table.concat {card.real_name, parallel_component[parallel]}, nil - end - - local xp_component = "" - if ((tonumber(card.xp) or 0) > 0) then - xp_component = table.concat {" (", card.xp, ")"} - end - - local taboo_component = "" - local cards = taboo.cards or {} - if (cards[card.code]) then - taboo_component = " (Taboo)" - end - - - local target_name = table.concat({ card.real_name, xp_component, taboo_component }) - local target_subname = discriminator or card.subname - - return target_name, target_subname -end - ----@param zone string ----@param count number ----@return string[] -local function fill_zone(zone, count) - local result = {} - for n=1,count do - result[n] = zone - end - return result -end - ----@param card ArkhamImportCard ----@param zone string[] ----@param override string[] ----@return string[] -local function get_zone_id(card, zone, override) - local result = {} - for n=1,card.count do - result[n] = zone[n] - or override[n] - or (card.is_bonded and "bonded") - or (card.permanent and "permanent") - or (card.subtype_name and card.subtype_name:find("Weakness") and "weakness") - or (card.type_code == "investigator" and "investigator") - or "default" - end - - return result -end - ----@param cards ArkhamImportCard[] ----@param deck ArkhamImportDeck ----@param command_manager TTSObject ----@param configuration ArkhamImportConfiguration -local function on_cards_ready(cards, deck, command_manager, configuration) - local card_bag = getObjectFromGUID(configuration.card_bag_guid) - local weakness_bag = getObjectFromGUID(configuration.weaknesses_bag_guid) - local investigator_bag = getObjectFromGUID(configuration.investigator_bag_guid) - local minicard_bag = getObjectFromGUID(configuration.minicard_bag_guid) - - local taboo = taboo_list[deck.taboo_id] or {} - - local meta = deck.meta and JSON.decode(deck.meta) or {} - - for _, card in ipairs(cards) do - ---@type ArkhamImport_Command_HandlerArguments - local parameters = { - configuration = configuration, - source_guid = self:getGUID(), - zone = {}, - card = card, - } - - ---@type ArkhamImport_CommandManager_HandlerResults - local command_result = command_manager:call("handle", parameters) - - if not command_result.is_successful then - debug_print(command_result.error_message, Priority.ERROR) - return - end - - local card = command_result.card - - if not command_result.handled then - local target_name, target_subname = get_card_selector(configuration.discriminators, card, taboo, meta) - local override = configuration.default_zone_overrides[card.code] - - if type(override)=="string" then override = fill_zone(override, card.count) end - - local zone = get_zone_id(card, command_result.zone, configuration.default_zone_overrides[card.code] or {}) - - local spawn_zones = {} - - local zones = configuration.zones - for index, zone in ipairs(zone) do - spawn_zones[index] = zones[zone] - end - - if card.real_name == "Random Basic Weakness" then - random_weakness(weakness_bag, spawn_zones) - elseif card.type_code == "investigator" then - process_card(investigator_bag, target_name, nil, card.count, spawn_zones) - process_card(minicard_bag, card.real_name, nil, card.count, spawn_zones) - else - process_card(card_bag, target_name, target_subname, card.count, spawn_zones) - end - end - end -end - ----@param deck ArkhamImportDeck ----@param configuration ArkhamImportConfiguration -local function on_deck_result(deck, configuration) - debug_print(table.concat({ "Found decklist: ", deck.name}), Priority.INFO) - - debug_print(table.concat({"-", deck.name, "-"}), Priority.DEBUG) - for k,v in pairs(deck) do - if type(v)=="table" then - debug_print(table.concat {k, ":
"}, Priority.DEBUG) - else - debug_print(table.concat {k, ": ", tostring(v)}, Priority.DEBUG) - end - end - debug_print("", Priority.DEBUG) - - local investigator_id = deck.investigator_code - - local slots = deck.slots - slots[investigator_id] = 1 - - ---@type ArkhamImportCard[] - local requests = load_cards(configuration, deck.slots) - - local command_manager = getObjectFromGUID(configuration.command_manager_guid) - - ---@type ArkhamImport_CommandManager_InitializationArguments - local parameters = { - configuration = configuration, - description = deck.description_md, - } - - ---@type ArkhamImport_CommandManager_InitializationResults - local results = command_manager:call("initialize", parameters) - - if not results.is_successful then - debug_print(results.error_message, Priority.ERROR) - return - end - - Request.with_all(requests, on_cards_ready, nil, deck, command_manager, results.configuration) -end - -function build_deck() - local configuration = get_configuration() - local deck_id = self:getInputs()[1].value - local deck_uri = { configuration.api_uri, is_private_deck and configuration.private_deck or configuration.public_deck, deck_id } - - local deck = Request.start(deck_uri, function (status) - if string.find(status.text, "") then - return false, table.concat({ "Private deck ", deck_id, " is not shared"}) - end - - local json = JSON.decode(status.text) - - if not json then - return false, "Deck not found!" - end - - return true, JSON.decode(status.text) - end) - - deck:with(on_deck_result, 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 - debug_print(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) -end \ No newline at end of file diff --git a/src/arkhamdb/MoveCommand.ttslua b/src/arkhamdb/MoveCommand.ttslua deleted file mode 100644 index f5feeaaa..00000000 --- a/src/arkhamdb/MoveCommand.ttslua +++ /dev/null @@ -1,70 +0,0 @@ ---- ---- Generated by EmmyLua(https://github.com/EmmyLua) ---- Created by Whimsical. ---- DateTime: 2021-08-24 6:02 p.m. ---- - -command_name = "move" - ----@type ArkhamImport_Command_RunDirectives -runOn = { - instructions = true, - handlers = true -} - ----@param parameters ArkhamImport_Command_DescriptionInstructionArguments ----@return ArkhamImport_Command_DescriptionInstructionResults -function do_instruction(parameters) - local args = parameters.arguments - - if (#args~=2 and #args~=3) then - return { is_successful = false, error_message = "Move Command requires 2 or 3 arguments. " .. #args .. " were provided." } - end - - local card_id = args[1] - local new_zone = args[2] - local count = tonumber(args[3]) or 3 - - if not parameters.configuration.zones[new_zone] then - return { is_successful = false, error_message = "Move Command: Zone \"" .. new_zone .. "\" was not found." } - end - - local state = parameters.command_state["move"] - - if not state then - state = {} - parameters.command_state["move"] = state - end - - local card_data = state[card_id] - - if not card_data then - card_data = { - zone = {}, - offset = 0 - } - - state[card_id] = card_data - end - - local zone = card_data.zone - local offset = card_data.offset - - for index=offset,offset+count do - zone[index] = new_zone - end - - return { command_state = parameters.command_state, is_successful = true } -end - ----@param parameters ArkhamImport_Command_HandlerArguments ----@return ArkhamImport_Command_HandlerResults -function handle_card(parameters) - local state = parameters.command_state["move"] or {} - - local card_data = state[parameters.card.code] - - if not card_data then return { is_successful = true} end - - return { zone = card_data.zone, is_successful = true } -end diff --git a/src/arkhamdb/ProxyCardCommand.ttslua b/src/arkhamdb/ProxyCardCommand.ttslua deleted file mode 100644 index bbd395ac..00000000 --- a/src/arkhamdb/ProxyCardCommand.ttslua +++ /dev/null @@ -1,91 +0,0 @@ ---- ---- Generated by EmmyLua(https://github.com/EmmyLua) ---- Created by Whimsical. ---- DateTime: 2021-08-24 6:11 p.m. ---- - -command_name = "proxy-card" - ----@type ArkhamImport_Command_RunDirectives -runOn = { - instructions = true, - handlers = true -} - -local back_image_default = "https://images-ext-2.discordapp.net/external/QY_dmo_UnAHEi1pgWwaRr1-HSB8AtrAv0W74Mh_Z6vg/https/i.imgur.com/EcbhVuh.jpg" - ----@param parameters ArkhamImport_Command_DescriptionInstructionArguments ----@return ArkhamImport_Command_DescriptionInstructionResults -function do_instruction(parameters) - local args = parameters.arguments - if (#args<4 or #args>6) then - return { - is_successful = false, - error_message = "Move Command requires between 4 or 6 arguments. " .. #args .. " were provided." - } - end - - if not parameters.command_state["proxy-card"] then - parameters.command_state["proxy-card"] = {} - parameters.command_state["proxy-card-offset"] = 0.1 - end - - parameters.command_state["proxy-card"][args[1]] = { - name = args[2], - subtitle = args[3], - image_uri = args[4], - zone = args[5] or "default", - back_image_uri = args[6] or back_image_default - } - - return { - command_state = parameters.command_state, - is_successful = true - } -end - ----@param parameters ArkhamImport_Command_HandlerArguments ----@return ArkhamImport_Command_HandlerResults -function handle_card(parameters) - local state = parameters.command_state["proxy-card"] or {} - - local card_data = state[parameters.card.code] - - if not card_data then return { is_successful = true } end - - local offset = parameters.command_state["proxy-card-offset"] - parameters.command_state["proxy-card-offset"] = offset + 0.1 - - local zone = parameters.configuration.zones[card_data.zone] - - if not zone then - return { - is_successful = false, - error_message = "Proxy Card [" .. tostring(parameters.card.code) .. "]: Zone \"" .. tostring(card_data.zone) .. "\" was not found." - } - end - - local source = getObjectFromGUID(parameters.source_guid) - local position = zone.is_absolute and zone.position or source:positionToWorld(zone.position) - - for _=1, parameters.card.count do - local new = spawnObject { - type = "CardCustom", - position = position + Vector(0, offset, 0), - rotation = source:getRotation() + Vector(0, 0, zone.is_facedown and 180 or 0), - ---@param card TTSObject - callback_function = function (card) - card:setName(card_data.name) - card:setDescription(card_data.subtitle) - end - } - - new:setCustomObject { - type = 0, - face = card_data.image_uri, - back = card_data.back_image_uri - } - end - - return { handled = true, is_successful = true } -end diff --git a/src/arkhamdb/ProxyInvestigatorCommand.ttslua b/src/arkhamdb/ProxyInvestigatorCommand.ttslua deleted file mode 100644 index a96a3981..00000000 --- a/src/arkhamdb/ProxyInvestigatorCommand.ttslua +++ /dev/null @@ -1,96 +0,0 @@ ---- ---- Generated by EmmyLua(https://github.com/EmmyLua) ---- Created by Whimsical. ---- DateTime: 2021-08-25 2:32 p.m. ---- - -command_name = "proxy-investigator" - ----@type ArkhamImport_Command_RunDirectives -runOn = { - instructions = true, - handlers = true -} - ----@param parameters ArkhamImport_Command_DescriptionInstructionArguments ----@return ArkhamImport_Command_DescriptionInstructionResults -function do_instruction(parameters) - local args = parameters.arguments - - if (#args~=6 and #args~=7) then - return { - is_successful = false, - error_message = "Proxy Investigator command requires either 7 or 8 arguments. " .. #args .. " were provided." - } - end - - parameters.command_state["proxy-investigator"] = { - name = args[1], - subtitle = args[2], - front_uri = args[3], - back_uri = args[4], - mini_front_uri = args[5], - mini_back_uri = args[6], - zone = args[7] or "investigator" - } - - return { - command_state = parameters.command_state, - is_successful = true - } -end - ----@param source TTSObject ----@param name string ----@param subtitle string ----@param offset number ----@param zone ArkhamImportZone ----@param front string ----@param back string ----@param use_minicard_scaling boolean -local function create_card(source, name, subtitle, offset, zone, front, back, use_minicard_scaling) - local position = zone.is_absolute and zone.position or source:positionToWorld(zone.position) - - local card = spawnObject { - type = "CardCustom", - position = position + Vector(0, offset, 0), - rotation = source:getRotation() + Vector(0, 0, zone.is_facedown and 180 or 0), - scale = use_minicard_scaling and Vector(0.6, 1, 0.6) or Vector(1,1,1), - callback_function = function (card) card:setName(name) card:setDescription(subtitle) end - } - - card:setCustomObject { - type = 0, - face = front, - back = back - } -end - ----@param parameters ArkhamImport_Command_HandlerArguments ----@return ArkhamImport_Command_HandlerResults -function handle_card(parameters) - if parameters.card.type_code ~= "investigator" then return {is_successful = true } end - - local card_data = parameters.command_state["proxy-investigator"] or {} - - if not card_data then return { is_successful = true } end - - local zone = parameters.configuration.zones[card_data.zone] - - if not zone then - return { - is_successful = false, - command_state = parameters.command_state, - error_message = "Proxy Investigator [" .. tostring(parameters.card.code) .. "]: Zone \"" .. tostring(card_data.zone) .. "\" was not found." - } - end - - local source = getObjectFromGUID(parameters.source_guid) - - for _=1, parameters.card.count do - create_card(source, card_data.name, card_data.subtitle, 10, zone, card_data.front_uri, card_data.back_uri, false) - create_card(source, card_data.name, card_data.subtitle, 20, zone, card_data.mini_front_uri, card_data.mini_back_uri, true) - end - - return { handled = true, is_successful = true} -end diff --git a/src/arkhamdb/RandomWeaknessGenerator.ttslua b/src/arkhamdb/RandomWeaknessGenerator.ttslua index f083210d..8ac17439 100644 --- a/src/arkhamdb/RandomWeaknessGenerator.ttslua +++ b/src/arkhamdb/RandomWeaknessGenerator.ttslua @@ -16,6 +16,10 @@ end function buttonClick_draw() local allCardsBag = getObjectFromGUID(allCardsBagGuid) local weaknessId = allCardsBag.call("getRandomWeaknessId") + if (weaknessId == nil) then + broadcastToAll("All basic weaknesses are in play!", {0.9, 0.2, 0.2}) + return + end local card = allCardsBag.call("getCardById", { id = weaknessId }) spawnObjectData({ data = card.data, diff --git a/src/arkhamdb/Zones.ttslua b/src/arkhamdb/Zones.ttslua deleted file mode 100644 index b05ea915..00000000 --- a/src/arkhamdb/Zones.ttslua +++ /dev/null @@ -1,171 +0,0 @@ --- Sets up and returns coordinates for all possible spawn zones. Because Lua --- assigns tables by reference and there is no built-in function to copy a --- table this is relatively brute force. --- --- Positions are all relative to the player mat, and most are consistent. The --- exception are the SetAside# zones, which are placed to the left of the mat --- for White/Green, and the right of the mat for Orange/Red. --- --- Valid Zones: --- Investigator: Investigator card area. --- Minicard: Placement for the investigator's minicard. This is just above the --- player mat, vertically in line with the investigator card area. --- Deck, Discard: Standard locations for the deck and discard piles. --- BlankTop, Tarot, Hand1, Hand2, Ally, BlankBottom, Accessory, Arcane1, --- Arcane2, Body: Asset slot positions on the player mat. --- Threat[1-4]: Threat area slots. Threat[1-3] correspond to the named threat --- area slots, and Threat4 is the blank threat area slot. --- SetAside[1-6]: Areas outside the player mat, to the right for Red/Orange and --- the left for White/Green. SetAside[1-3] are a column closest to the --- player mat, with 1 at the top of the mat and 3 at the bottom. --- SetAside[4-6] are a column farther away from the mat, with 4 at the top --- and 6 at the bottom. -do - local playerMatGuids = { } - playerMatGuids["Red"] = "0840d5" - playerMatGuids["Orange"] = "bd0ff4" - playerMatGuids["White"] = "8b081b" - playerMatGuids["Green"] = "383d8b" - - local Zones = { } - - local commonZones = { } - commonZones["Investigator"] = {-0.7833852, 0, 0.0001343352} - commonZones["Minicard"] = {-0.7833852, 0, -1.187242} - commonZones["Deck"] = {-1.414127, 0, -0.006668129} - commonZones["Discard"] = {-1.422189,0,0.643440} - commonZones["Ally"] = {-0.236577,0,0.023543} - commonZones["Body"] = {-0.257249,0,0.553170} - commonZones["Hand1"] = {0.600493,0,0.037291} - commonZones["Hand2"] = {0.206867,0,0.025540} - commonZones["Arcane1"] = {0.585817,0,0.567969} - commonZones["Arcane2"] = {0.197267,0,0.562296} - commonZones["Tarot"] = {0.980616,0,0.047756} - commonZones["Accessory"] = {0.976689,0,0.569344} - commonZones["BlankTop"] = {1.364696,0,0.062552} - commonZones["BlankBottom"] = {1.349999,0,0.585419} - commonZones["Threat1"] = {-0.835423,0,-0.633271} - commonZones["Threat2"] = {-0.384615,0,-0.633493} - commonZones["Threat3"] = {0.071090,0,-0.633717} - commonZones["Threat4"] = {0.520816,0,-0.633936} - - Zones["White"] = { } - Zones["White"]["Investigator"] = commonZones["Investigator"] - Zones["White"]["Minicard"] = commonZones["Minicard"] - Zones["White"]["Deck"] = commonZones["Deck"] - Zones["White"]["Discard"] = commonZones["Discard"] - Zones["White"]["Ally"] = commonZones["Ally"] - Zones["White"]["Body"] = commonZones["Body"] - Zones["White"]["Hand1"] = commonZones["Hand1"] - Zones["White"]["Hand2"] = commonZones["Hand2"] - Zones["White"]["Arcane1"] = commonZones["Arcane1"] - Zones["White"]["Arcane2"] = commonZones["Arcane2"] - Zones["White"]["Tarot"] = commonZones["Tarot"] - Zones["White"]["Accessory"] = commonZones["Accessory"] - Zones["White"]["BlankTop"] = commonZones["BlankTop"] - Zones["White"]["BlankBottom"] = commonZones["BlankBottom"] - Zones["White"]["Threat1"] = commonZones["Threat1"] - Zones["White"]["Threat2"] = commonZones["Threat2"] - Zones["White"]["Threat3"] = commonZones["Threat3"] - Zones["White"]["Threat4"] = commonZones["Threat4"] - Zones["White"]["SetAside1"] = {2.004500,0,-0.520315} - Zones["White"]["SetAside2"] = {2.004500,0,0.042552} - Zones["White"]["SetAside3"] = {2.004500,0,0.605419} - Zones["White"]["UnderSetAside3"] = {2.154500,0,0.805419} - Zones["White"]["SetAside4"] = {2.434500,0,-0.520315} - Zones["White"]["SetAside5"] = {2.434500,0,0.042552} - Zones["White"]["SetAside6"] = {2.434500,0,0.605419} - Zones["White"]["UnderSetAside6"] = {2.584500,0,0.805419} - Zones["Orange"] = { } - Zones["Orange"]["Investigator"] = commonZones["Investigator"] - Zones["Orange"]["Minicard"] = commonZones["Minicard"] - Zones["Orange"]["Deck"] = commonZones["Deck"] - Zones["Orange"]["Discard"] = commonZones["Discard"] - Zones["Orange"]["Ally"] = commonZones["Ally"] - Zones["Orange"]["Body"] = commonZones["Body"] - Zones["Orange"]["Hand1"] = commonZones["Hand1"] - Zones["Orange"]["Hand2"] = commonZones["Hand2"] - Zones["Orange"]["Arcane1"] = commonZones["Arcane1"] - Zones["Orange"]["Arcane2"] = commonZones["Arcane2"] - Zones["Orange"]["Tarot"] = commonZones["Tarot"] - Zones["Orange"]["Accessory"] = commonZones["Accessory"] - Zones["Orange"]["BlankTop"] = commonZones["BlankTop"] - Zones["Orange"]["BlankBottom"] = commonZones["BlankBottom"] - Zones["Orange"]["Threat1"] = commonZones["Threat1"] - Zones["Orange"]["Threat2"] = commonZones["Threat2"] - Zones["Orange"]["Threat3"] = commonZones["Threat3"] - Zones["Orange"]["Threat4"] = commonZones["Threat4"] - Zones["Orange"]["SetAside1"] = {-2.004500,0,-0.520315} - Zones["Orange"]["SetAside2"] = {-2.004500,0,0.042552} - Zones["Orange"]["SetAside3"] = {-2.004500,0,0.605419} - Zones["Orange"]["UnderSetAside3"] = {-2.154500,0,0.80419} - Zones["Orange"]["SetAside4"] = {-2.434500,0,-0.520315} - Zones["Orange"]["SetAside5"] = {-2.434500,0,0.042552} - Zones["Orange"]["SetAside6"] = {-2.434500,0,0.605419} - Zones["Orange"]["UnderSetAside6"] = {-2.584500,0,0.80419} - - -- Green positions are the same as White, and Red the same as orange, so we - -- can just point these at the White/Orange definitions - Zones["Red"] = Zones["Orange"] - Zones["Green"] = Zones["White"] - - -- 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 Zones.getDefaultCardZone(cardMetadata) - if (cardMetadata.type == "Investigator") then - return "Investigator" - elseif (cardMetadata.type == "Minicard") then - return "Minicard" - elseif (cardMetadata.permanent) then - return "SetAside1" - elseif (cardMetadata.bonded_to ~= nil) then - return "SetAside2" - else - return "Deck" - end - end - - -- Gets the global position for the given zone on the specified player mat. - -- Param playerColor: Color name of the player mat to get the zone position - -- for (e.g. "Red") - -- Param zoneName: Name of the zone to get the position for. See Zones object - -- documentation for a list of valid zones. - -- Return: Global position table, or nil if an invalid player color or zone - -- is specified - function Zones.getZonePosition(playerColor, zoneName) - if (playerColor ~= "Red" - and playerColor ~= "Orange" - and playerColor ~= "White" - and playerColor ~= "Green") then - return nil - end - return getObjectFromGUID(playerMatGuids[playerColor]).positionToWorld(Zones[playerColor][zoneName]) - end - - -- Return the global rotation for a card on the given player mat, based on its - -- metadata. - -- Param playerColor: Color name of the player mat to get the rotation - -- for (e.g. "Red") - -- Param cardMetadata: Table of card metadata. Metadata fields type and - -- permanent are required; all others are optional. - -- Return: Global rotation vector for the given card. This will include the - -- Y rotation to orient the card on the given player mat as well as a - -- Z rotation to place the card face up or face down. - function Zones.getDefaultCardRotation(playerColor, zone) - local deckRotation = getObjectFromGUID(playerMatGuids[playerColor]).getRotation() - - if (zone == "Investigator") then - deckRotation = deckRotation + Vector(0, 270, 0) - elseif (zone == "Deck") then - deckRotation = deckRotation + Vector(0, 0, 180) - end - - return deckRotation - end - - return Zones -end diff --git a/src/chaosbag/StatTracker.ttslua b/src/chaosbag/ChaosBagStatTracker.ttslua similarity index 100% rename from src/chaosbag/StatTracker.ttslua rename to src/chaosbag/ChaosBagStatTracker.ttslua diff --git a/src/core/CustomDataHelper.ttslua b/src/core/CustomDataHelper.ttslua index e69de29b..63742976 100644 --- a/src/core/CustomDataHelper.ttslua +++ b/src/core/CustomDataHelper.ttslua @@ -0,0 +1,76 @@ +-- set true to enable debug logging +DEBUG = false + +function log(message) + if DEBUG then + print(message) + end +end + +--[[ +Known locations and clues. We check this to determine if we should +atttempt to spawn clues, first we look for _ and if +we find nothing we look for +format is [location_guid -> clueCount] +]] +LOCATIONS_DATA_JSON = [[ +{ + "San Francisco": {"type": "fixed", "value": 1, "clueSide": "back"}, + " Arkham": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Buenos Aires": {"type": "fixed", "value": 2, "clueSide": "back"}, + " London": {"type": "perPlayer", "value": 2, "clueSide": "front"}, + "Rome": {"type": "perPlayer", "value": 3, "clueSide": "front"}, + "Istanbul": {"type": "perPlayer", "value": 4, "clueSide": "front"}, + "Tokyo_123abc": {"type": "perPlayer", "value": 0, "clueSide": "back"}, + "Tokyo_456efg": {"type": "perPlayer", "value": 4, "clueSide": "back"}, + "Tokyo": {"type": "fixed", "value": 2, "clueSide": "back"}, + "Shanghai_123": {"type": "fixed", "value": 12, "clueSide": "front"}, + "Sydney": {"type": "fixed", "value": 0, "clueSide": "front"} +} +]] + + +PLAYER_CARD_DATA_JSON = [[ +{ + "Tool Belt (0)": { + "tokenType": "resource", + "tokenCount": 2 + }, + "Tool Belt (3)": { + "tokenType": "resource", + "tokenCount": 4 + }, + "Yithian Rifle": { + "tokenType": "resource", + "tokenCount": 3 + }, + "xxx": { + "tokenType": "resource", + "tokenCount": 3 + } +} +]] + +HIDDEN_CARD_DATA = { + "Unpleasant Card (Doom)", + "Unpleasant Card (Gloom)", + "The Case of the Scarlet DOOOOOM!" +} + +LOCATIONS_DATA = JSON.decode(LOCATIONS_DATA_JSON) +PLAYER_CARD_DATA = JSON.decode(PLAYER_CARD_DATA_JSON) + +function onload(save_state) + local playArea = getObjectFromGUID('721ba2') + playArea.call("updateLocations", {self.getGUID()}) + local playerMatWhite = getObjectFromGUID('8b081b') + playerMatWhite.call("updatePlayerCards", {self.getGUID()}) + local playerMatOrange = getObjectFromGUID('bd0ff4') + playerMatOrange.call("updatePlayerCards", {self.getGUID()}) + local playerMatGreen = getObjectFromGUID('383d8b') + playerMatGreen.call("updatePlayerCards", {self.getGUID()}) + local playerMatRed = getObjectFromGUID('0840d5') + playerMatRed.call("updatePlayerCards", {self.getGUID()}) + local dataHelper = getObjectFromGUID('708279') + dataHelper.call("updateHiddenCards", {self.getGUID()}) +end diff --git a/src/core/DataHelper.ttslua b/src/core/DataHelper.ttslua index 6b711ba2..22f926dc 100644 --- a/src/core/DataHelper.ttslua +++ b/src/core/DataHelper.ttslua @@ -636,6 +636,82 @@ LOCATIONS_DATA_JSON = [[ "Wine Cellar_b882f3": {"type": "perPlayer", "value": 2, "clueSide": "back"}, "Hidden Passageway": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Frozen Shores": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Treacherous Path": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Precarious Ice Sheet": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Broad Snowdrifts": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + "Icy Wastes": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + "Rocky Crags": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + "Snow Graves": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + "Icebreaker Landing": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + "Frigid Cave": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + "Barrier Camp": {"type": "perPlayer", "value": 3, "clueSide": "back"}, + "Remnants of Lake's Camp": {"type": "perPlayer", "value": 3, "clueSide": "back"}, + "Crystalline Cavern": {"type": "perPlayer", "value": 3, "clueSide": "back"}, + + "Prison of Memories": {"type": "perPlayer", "value": 3, "clueSide": "front"}, + "Base Camp": {"type": "perPlayer", "value": 3, "clueSide": "front"}, + "Deck of the Theodosia": {"type": "perPlayer", "value": 3, "clueSide": "front"}, + "University Halls": {"type": "perPlayer", "value": 3, "clueSide": "front"}, + "Hedge Maze": {"type": "perPlayer", "value": 2, "clueSide": "front"}, + "Deserted Station": {"type": "perPlayer", "value": 2, "clueSide": "front"}, + "Hedge Maze": {"type": "perPlayer", "value": 2, "clueSide": "front"}, + "Coastal Waters": {"type": "perPlayer", "value": 4, "clueSide": "front"}, + "Elder Chamber": {"type": "perPlayer", "value": 2, "clueSide": "front"}, + "Riverview Theatre": {"type": "perPlayer", "value": 4, "clueSide": "front"}, + "Standing Stones": {"type": "perPlayer", "value": 4, "clueSide": "front"}, + "Airfield": {"type": "perPlayer", "value": 2, "clueSide": "front"}, + "Alaskan Wilds": {"type": "perPlayer", "value": 2, "clueSide": "front"}, + "Cluttered Dormitory": {"type": "perPlayer", "value": 2, "clueSide": "front"}, + "Dyer's Classroom": {"type": "perPlayer", "value": 2, "clueSide": "front"}, + "Infirmary_80c56d": {"type": "perPlayer", "value": 2, "clueSide": "front"}, + "Dr. Kensler's Office": {"type": "perPlayer", "value": 2, "clueSide": "front"}, + "Moʻai Statues": {"type": "perPlayer", "value": 2, "clueSide": "front"}, + "Ottoman Front": {"type": "perPlayer", "value": 2, "clueSide": "front"}, + "The Black Stone": {"type": "perPlayer", "value": 2, "clueSide": "front"}, + + "The Summit": {"type": "perPlayer", "value": 3, "clueSide": "back"}, + "Mountainside": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + "Mountainside_0dd2ac": {"type": "perPlayer", "value": 0, "clueSide": "back"}, + "Mountainside_62fb7b": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Mountainside_0a512e": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Mountainside_163ba8": {"type": "perPlayer", "value": 3, "clueSide": "back"}, + + "Hidden Tunnel": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + "City Landscape": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "City Landscape_ec2d80": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + "City Landscape_d84841": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + + + "Ancient Facility": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Ancient Facility_97d88d": {"type": "perPlayer", "value": 0, "clueSide": "back"}, + "Ancient Facility_0ff8d1": {"type": "perPlayer", "value": 0, "clueSide": "back"}, + "Ancient Facility_42f1ad": {"type": "perPlayer", "value": 0, "clueSide": "back"}, + "Ancient Facility_710850": {"type": "perPlayer", "value": 0, "clueSide": "back"}, + "Ancient Facility_27771a": {"type": "perPlayer", "value": 0, "clueSide": "back"}, + "Ancient Facility_f9fc4d": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + "Ancient Facility_baf524": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + "Ancient Facility_c70271": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + "The Gate of Y'quaa": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Mist-Pylon": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + "Mist-Pylon_9ca053": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Mist-Pylon_c320b1": {"type": "perPlayer", "value": 3, "clueSide": "back"}, + "Mist-Pylon_cc2b13": {"type": "perPlayer", "value": 4, "clueSide": "back"}, + + "River Docks": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Miskatonic University_cf8d9e": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Miskatonic University_b6c3a5": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Miskatonic University_fb6a7c": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Arkham Gazette": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Arkham Advertiser": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "O'Malley's Watch Shop": {"type": "perPlayer", "value": 3, "clueSide": "back"}, + "Tick-Tock Club_e1116a": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + "Tick-Tock Club": {"type": "perPlayer", "value": 3, "clueSide": "back"}, + + "Childhood Home": {"type": "perPlayer", "value": 2, "clueSide": "back"}, + "Ye Olde Magick Shoppe": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "Corrigan Industries": {"type": "perPlayer", "value": 1, "clueSide": "back"}, + "XXXX": {"type": "fixed", "value": 2, "clueSide": "back"}, "xxx": {"type": "perPlayer", "value": 2, "clueSide": "back"} } @@ -645,712 +721,12 @@ Player cards with token counts and types ]] PLAYER_CARD_DATA_JSON = [[ { - "Flashlight": { + "xxx": { "tokenType": "resource", "tokenCount": 3 }, - "Shrivelling": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Shrivelling (3)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Grotesque Statue (4)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Forbidden Knowledge": { - "tokenType": "resource", - "tokenCount": 4 - }, - ".45 Automatic": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Shotgun (4)": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Liquid Courage": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Song of the Dead (2)": { - "tokenType": "resource", - "tokenCount": 5 - }, - "Cover Up": { - "tokenType": "clue", - "tokenCount": 3 - }, - "Roland's .38 Special": { - "tokenType": "resource", - "tokenCount": 4 - }, - "First Aid": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Scrying": { - "tokenType": "resource", - "tokenCount": 3 - }, - ".41 Derringer": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Painkillers": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Smoking Pipe": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Clarity of Mind": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Rite of Seeking": { - "tokenType": "resource", - "tokenCount": 3 - }, - "M1918 BAR (4)": { - "tokenType": "resource", - "tokenCount": 8 - }, - "Ornate Bow (3)": { - "tokenType": "resource", - "tokenCount": 1 - }, - ".41 Derringer (2)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Suggestion (4)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Chicago Typewriter (4)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Lupara (3)": { - "tokenType": "resource", - "tokenCount": 2 - }, - "First Aid (3)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Springfield M1903 (4)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Springfield M1903 (4) (Taboo)": { - "tokenType": "resource", - "tokenCount": 3 - }, - ".32 Colt": { - "tokenType": "resource", - "tokenCount": 6 - }, - "Venturer": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Lockpicks (1)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Finn's Trusty .38": { - "tokenType": "resource", - "tokenCount": 3 - }, - ".45 Automatic (2)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Lightning Gun (5)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Strange Solution (4)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Strange Solution (4):Acidic Ichor": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Strange Solution (4):Empowering Elixir": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Arcane Insight (4)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Archaic Glyphs (3)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "In the Know (1)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Rite of Seeking (4)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Alchemical Transmutation": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Scrying (3)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Shrivelling (5)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Mists of R'lyeh": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Mists of R'lyeh (4)": { - "tokenType": "resource", - "tokenCount": 5 - }, - "Colt Vest Pocket": { - "tokenType": "resource", - "tokenCount": 5 - }, - "Old Hunting Rifle (3)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Thermos": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Feed the Mind (3)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Seal of the Seventh Sign (5)": { - "tokenType": "resource", - "tokenCount": 7 - }, - "Flamethrower (5)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Flamethrower (5) (Taboo)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Pnakotic Manuscripts (5)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Kerosene (1)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Shards of the Void (3)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Try and Try Again (1)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Arcane Initiate": { - "tokenType": "doom", - "tokenCount": 1 - }, - "Detective's Colt 1911s": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Extra Ammunition (1)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Rite of Seeking (2)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Arcane Initiate (3)": { - "tokenType": "doom", - "tokenCount": 1 - }, - "Clarity of Mind (3)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Fingerprint Kit": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Truth from Fiction": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Enchanted Blade": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Tennessee Sour Mash": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Scroll of Secrets": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Scroll of Secrets (Taboo)": { - "tokenType": "resource", - "tokenCount": 3 - }, - ".45 Thompson": { - "tokenType": "resource", - "tokenCount": 5 - }, - "Mr. \"Rook\"": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Mr. \"Rook\" (Taboo)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Scroll of Secrets (3):Seeker": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Scroll of Secrets (3) (Taboo):Seeker": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Scroll of Secrets (3):Mystic": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Scroll of Secrets (3) (Taboo):Mystic": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Enchanted Blade (3):Guardian": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Enchanted Blade (3):Mystic": { - "tokenType": "resource", - "tokenCount": 4 - }, - ".45 Thompson (3)": { - "tokenType": "resource", - "tokenCount": 5 - }, - "Esoteric Atlas (1)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Tennessee Sour Mash (3):Rogue": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Tennessee Sour Mash (3):Survivor": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Mk 1 Grenades (4)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Dayana Esperence (3)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Pendant of the Queen": { - "tokenType": "resource", - "tokenCount": 3 - }, - ".32 Colt (2)": { - "tokenType": "resource", - "tokenCount": 6 - }, - "Alchemical Transmutation (2)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Suggestion (1)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Gate Box": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Tony's .38 Long Colt": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Gregory Gry": { - "tokenType": "resource", - "tokenCount": 9 - }, - "Scroll of Prophecies": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Healing Words": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Otherworld Codex (2)": { - "tokenType": "resource", - "tokenCount": 3 - }, - ".35 Winchester": { - "tokenType": "resource", - "tokenCount": 5 - }, - ".35 Winchester (Taboo)": { - "tokenType": "resource", - "tokenCount": 5 - }, - "Old Book of Lore (3)": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Sawed-Off Shotgun (5)": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Mind's Eye (2)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Colt Vest Pocket (2)": { - "tokenType": "resource", - "tokenCount": 5 - }, - "Mists of R'lyeh (2)": { - "tokenType": "resource", - "tokenCount": 5 - }, - "The Chthonian Stone (3)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Flesh Ward": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Physical Training (4)": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Encyclopedia": { - "tokenType": "resource", - "tokenCount": 5 - }, - "Feed the Mind": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Forbidden Tome": { - "tokenType": "resource", - "tokenCount": 5 - }, - "Esoteric Atlas (2)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "The Necronomicon (5)": { - "tokenType": "resource", - "tokenCount": 6 - }, - "The Necronomicon (5) (Taboo)": { - "tokenType": "resource", - "tokenCount": 6 - }, - "Mauser C96": { - "tokenType": "resource", - "tokenCount": 5 - }, - "Liquid Courage (1)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Mauser C96 (2)": { - "tokenType": "resource", - "tokenCount": 5 - }, - "Beretta M1918 (4)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Scrying Mirror": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Azure Flame": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Clairvoyance": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Ineffable Truth": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Grotesque Statue (2)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Azure Flame (3)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Clairvoyance (3)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Ineffable Truth (3)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Arcane Studies (4)": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Azure Flame (5)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Clairvoyance (5)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Ineffable Truth (5)": { - "tokenType": "resource", - "tokenCount": 3 - }, - ".18 Derringer": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Grimm's Fairy Tales": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Old Keyring": { - "tokenType": "resource", - "tokenCount": 2 - }, - ".18 Derringer (2)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Chainsaw (4)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Becky": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Book of Psalms": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Cryptographic Cipher": { - "tokenType": "resource", - "tokenCount": 3 - }, - ".25 Automatic": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Obfuscation": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Eldritch Sophist": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Armageddon": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Eye of Chaos": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Shroud of Shadows": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Guided by the Unseen (3)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Eye of Chaos (4)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Shroud of Shadows (4)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Armageddon (4)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Hyperawareness (4)": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Hard Knocks (4)": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Dig Deep (4)": { - "tokenType": "resource", - "tokenCount": 2 - }, - ".25 Automatic (2)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Shrine of the Moirai (3)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Archive of Conduits": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Archive of Conduits (4)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Eon Chart (1)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Eon Chart (4)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Brand of Cthugha (1)": { - "tokenType": "resource", - "tokenCount": 6 - }, - "Brand of Cthugha (4)": { - "tokenType": "resource", - "tokenCount": 9 - }, - "True Magick (5)": { - "tokenType": "resource", - "tokenCount": 1 - }, - "Healing Words (3)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Close the Circle (1)": { - "tokenType": "resource", - "tokenCount": 1 - }, - "Bangle of Jinxes (1)": { - "tokenType": "resource", - "tokenCount": 1 - }, - "Jury-Rig": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Bandages": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Schoffner's Catalogue": { - "tokenType": "resource", - "tokenCount": 5 - }, - "Antiquary (3)": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Crafty (3)": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Bruiser (3)": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Sleuth (3)": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Prophetic (3)": { - "tokenType": "resource", - "tokenCount": 2 - }, - "Earthly Serenity (4)": { - "tokenType": "resource", - "tokenCount": 6 - }, - "Earthly Serenity (1)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Enchanted Bow (2)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Blur (4)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Blur (1)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Professor William Webb (2)": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Professor William Webb": { - "tokenType": "resource", - "tokenCount": 3 - }, - "Divination (4)": { - "tokenType": "resource", - "tokenCount": 6 - }, - "Divination (1)": { - "tokenType": "resource", - "tokenCount": 4 - }, - "Cover Up:Advanced": { - "tokenType": "clue", - "tokenCount": 4 - }, - "xxx": { + "yyy": { "tokenType": "resource", "tokenCount": 3 } @@ -1648,6 +1024,21 @@ modeData = { standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'elder', 'red', 'blue' } } }, + -----------------Edge of the Earth + ['Edge of the Earth'] = { + easy = { token = { 'p1', 'p1', 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }, + normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }, + hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'frost', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }, + expert = { token = { '0', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'm7', 'frost', 'frost', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } } + }, + + ['City of the Elder Things'] = { + easy = { token = { 'p1', 'p1', 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }, + normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }, + hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'frost', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }, + expert = { token = { '0', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'm7', 'frost', 'frost', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } } + }, + -----------------The Side Missions --official ['Curse of the Rougarou'] = { @@ -1704,6 +1095,20 @@ modeData = { expert = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } } }, + ['Machinations'] = { + easy = { token = { 'p1', 'p1', 'p1', '0', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }, + normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }, + hard = { token = { '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'elder', 'red', 'blue' } }, + expert = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'elder', 'red', 'blue' } } + }, + + ['Red Tide'] = { + easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } }, + normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } }, + hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } }, + expert = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } } + }, + --fan-made ['Carnevale of Spiders'] = { normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }, diff --git a/src/core/DoomInPlayCounter.ttslua b/src/core/DoomInPlayCounter.ttslua new file mode 100644 index 00000000..f5448079 --- /dev/null +++ b/src/core/DoomInPlayCounter.ttslua @@ -0,0 +1,132 @@ +MIN_VALUE = -99 +MAX_VALUE = 999 + +function onload(saved_data) + light_mode = false + val = 0 + + if saved_data ~= "" then + local loaded_data = JSON.decode(saved_data) + light_mode = loaded_data[1] + val = loaded_data[2] + end + + createAll() +end + +function updateSave() + local data_to_save = {light_mode, val} + saved_data = JSON.encode(data_to_save) + self.script_state = saved_data +end + +function createAll() + s_color = {0.5, 0.5, 0.5, 95} + + if light_mode then + f_color = {1,1,1,95} + else + f_color = {0,0,0,100} + end + + + + self.createButton({ + label=tostring(val), + click_function="add_subtract", + function_owner=self, + position={0,0.05,0}, + height=600, + width=1000, + alignment = 3, + scale={x=1.5, y=1.5, z=1.5}, + font_size=600, + font_color=f_color, + color={0,0,0,0} + }) + + + + + if light_mode then + lightButtonText = "[ Set dark ]" + else + lightButtonText = "[ Set light ]" + end + +end + +function removeAll() + self.removeInput(0) + self.removeInput(1) + self.removeButton(0) + self.removeButton(1) + self.removeButton(2) +end + +function reloadAll() + removeAll() + createAll() + + updateSave() +end + +function swap_fcolor(_obj, _color, alt_click) + light_mode = not light_mode + reloadAll() +end + +function swap_align(_obj, _color, alt_click) + center_mode = not center_mode + reloadAll() +end + +function editName(_obj, _string, value) + self.setName(value) + setTooltips() +end + +function add_subtract(_obj, _color, alt_click) + mod = alt_click and -1 or 1 + new_value = math.min(math.max(val + mod, MIN_VALUE), MAX_VALUE) + if val ~= new_value then + val = new_value + updateVal() + updateSave() + end +end + +function updateVal() + + self.editButton({ + index = 0, + label = tostring(val), + + }) +end + +function reset_val() + val = 0 + updateVal() + updateSave() +end + +function setTooltips() + self.editInput({ + index = 0, + value = self.getName(), + tooltip = ttText + }) + self.editButton({ + index = 0, + value = tostring(val), + tooltip = ttText + }) +end + +function null() +end + +function keepSample(_obj, _string, value) + reloadAll() +end diff --git a/src/core/Global.ttslua b/src/core/Global.ttslua index f980e4c9..a14704bb 100644 --- a/src/core/Global.ttslua +++ b/src/core/Global.ttslua @@ -37,7 +37,9 @@ function onload() TOKEN_DATA = { clue = {image = tokenplayerone.clue, scale = {0.15, 0.15, 0.15}}, resource = {image = tokenplayerone.resource, scale = {0.17, 0.17, 0.17}}, - doom = {image = tokenplayerone.doom, scale = {0.17, 0.17, 0.17}} + doom = {image = tokenplayerone.doom, scale = {0.17, 0.17, 0.17}}, + damage = {image = tokenplayerone.damageone, scale = {0.17, 0.17, 0.17}}, + horror = {image = tokenplayerone.horrorone, scale = {0.17, 0.17, 0.17}} } getObjectFromGUID("6161b4").interactable=false @@ -49,6 +51,7 @@ function onload() getObjectFromGUID("9487a4").interactable=false getObjectFromGUID("91dd9b").interactable=false getObjectFromGUID("f182ee").interactable=false + getObjectFromGUID("7bff34").interactable=false end @@ -134,6 +137,8 @@ PULLS = { ["http://cloud-3.steamusercontent.com/ugc/1655601092778627699/339FB716CB25CA6025C338F13AFDFD9AC6FA8356/"] = 0, -- elder sign ["https://i.imgur.com/nEmqjmj.png"] = 0, + -- frost + ["http://cloud-3.steamusercontent.com/ugc/1858293462583104677/195F93C063A8881B805CE2FD4767A9718B27B6AE/"] = 0, } IMAGE_TOKEN_MAP = { @@ -172,7 +177,9 @@ IMAGE_TOKEN_MAP = { -- bless ["http://cloud-3.steamusercontent.com/ugc/1655601092778627699/339FB716CB25CA6025C338F13AFDFD9AC6FA8356/"] = "Bless", -- curse - ["http://cloud-3.steamusercontent.com/ugc/1655601092778636039/2A25BD38E8C44701D80DD96BF0121DA21843672E/"] = "Curse" + ["http://cloud-3.steamusercontent.com/ugc/1655601092778636039/2A25BD38E8C44701D80DD96BF0121DA21843672E/"] = "Curse", + -- frost + ["http://cloud-3.steamusercontent.com/ugc/1858293462583104677/195F93C063A8881B805CE2FD4767A9718B27B6AE/"] = "Frost" } function resetStats() @@ -686,6 +693,7 @@ function getImageUrl(id) if id == 'elder' then return 'https://i.imgur.com/ttnspKt.png' end if id == 'red' then return 'https://i.imgur.com/lns4fhz.png' end if id == 'blue' then return 'https://i.imgur.com/nEmqjmj.png' end + if id == 'frost' then return 'http://cloud-3.steamusercontent.com/ugc/1858293462583104677/195F93C063A8881B805CE2FD4767A9718B27B6AE/' end return '' end @@ -766,3 +774,199 @@ function updateRandomSeed() math.randomseed(os.time()) end end + + +-- Content Importing + + +--- Loadable Items test + +local source_repo = 'https://raw.githubusercontent.com/seth-sced/loadable-objects/main' +local list_url = 'library.json' +local library = nil + +local request_obj + +--- + +function get_source_repo() + return source_repo +end + +--- + +function onClick_toggleUi(player, window) + toggle_ui(window) +end + +function onClick_refreshList() + local request = WebRequest.get(get_source_repo() .. '/' .. list_url, completed_list_update) + request_obj = request + startLuaCoroutine(Global, 'my_coroutine') +end + +function onClick_select(player, params) + params = JSON.decode(urldecode(params)) + local url = get_source_repo() .. '/' .. params.url + local request = WebRequest.get(url, function (request) complete_obj_download(request, params) end ) + request_obj = request + startLuaCoroutine(Global, 'my_coroutine') +end + +function onClick_load() + UI.show('progress_display') + UI.hide('load_button') +end + +function onClick_cancel() +end + +--- + +function toggle_ui(title) + UI.hide('load_ui') + if UI.getValue('title') == title or title == 'Hidden' then + UI.setValue('title', 'Hidden') + else + UI.setValue('title', title) + update_window_content(title) + UI.show('load_ui') + end +end + +function my_coroutine() + while request_obj do + UI.setAttribute('download_progress', 'percentage', request_obj.download_progress * 100) + coroutine.yield(0) + end + return 1 +end + + +function update_list(objects) + local ui = UI.getXmlTable() + local update_height = find_tag_with_id(ui, 'ui_update_height') + local update_children = find_tag_with_id(update_height.children, 'ui_update_point') + + update_children.children = {} + + for i,v in ipairs(objects) do + local s = JSON.encode(v); + --print(s) + table.insert(update_children.children, + { + tag = 'Text', + value = v.name, + attributes = { onClick = 'onClick_select('.. urlencode(JSON.encode(v)) ..')', + alignment = 'MiddleLeft' } + } + ) + end + + update_height.attributes.height = #(update_children.children) * 24 + UI.setXmlTable(ui) +end + +function update_window_content(new_title) + if not library then + return + end + + if new_title == 'Campaigns' then + update_list(library.campaigns) + elseif new_title == 'Standalone Scenarios' then + update_list(library.scenarios) + elseif new_title == 'Investigators' then + update_list(library.investigators) + elseif new_title == 'Community Content' then + update_list(library.community) + elseif new_title == 'Extras' then + update_list(library.extras) + else + update_list({}) + end +end + +function complete_obj_download(request, params) + assert(request.is_done) + if request.is_error or request.response_code ~= 200 then + print('error: ' .. request.error) + else + if pcall(function () + local replaced_object + pcall(function () + if params.replace then + replaced_object = getObjectFromGUID(params.replace) + end + end) + local json = request.text + if replaced_object then + local pos = replaced_object.getPosition() + local rot = replaced_object.getRotation() + destroyObject(replaced_object) + Wait.frames(function () spawnObjectJSON({json = json, position = pos, rotation = rot}) end, 1) + else + spawnObjectJSON({json = json}) + end + end) then + print('Object loaded.') + else + print('Error loading object.') + end + end + + request_obj = nil + UI.setAttribute('download_progress', 'percentage', 100) + +end + +-- the download button on the placeholder objects calls this to directly initiate a download +function placeholder_download(params) + -- params is a table with url and guid of replacement object, which happens to match what onClick_select wants + onClick_select(nil, JSON.encode(params)) +end + +function completed_list_update(request) + assert(request.is_done) + if request.is_error or request.response_code ~= 200 then + print('error: ' .. request.error) + else + local json_response = nil + if pcall(function () json_response = JSON.decode(request.text) end) then + library = json_response + update_window_content(UI.getValue('title')) + else + print('error parsing downloaded library') + end + end + + request_obj = nil + UI.setAttribute('download_progress', 'percentage', 100) +end + +--- + +function find_tag_with_id(ui, id) + for i,obj in ipairs(ui) do + if obj.attributes and obj.attributes.id and obj.attributes.id == id then + return obj + end + if obj.children then + local result = find_tag_with_id(obj.children, id) + if result then return result end + end + end + return nil +end + +function urlencode(str) + str = string.gsub(str, "([^A-Za-z0-9-_.~])", + function (c) return string.format("%%%02X", string.byte(c)) end) + return str +end + +function urldecode(str) + str = string.gsub(str, "%%(%x%x)", + function (h) return string.char(tonumber(h, 16)) end) + return str +end diff --git a/src/core/PlayAreaSelector.ttslua b/src/core/PlayAreaSelector.ttslua index 164a305f..9439e0d6 100644 --- a/src/core/PlayAreaSelector.ttslua +++ b/src/core/PlayAreaSelector.ttslua @@ -1,199 +1,116 @@ - - -function onSave() - saved_data = JSON.encode({tid=tableImageData, cd=checkData}) - --saved_data = "" - return saved_data -end - -function onload(saved_data) - --Loads the tracking for if the game has started yet - if saved_data ~= "" then - local loaded_data = JSON.decode(saved_data) - tableImageData = loaded_data.tid - checkData = loaded_data.cd - else - tableImageData = {} - checkData = {move=false, scale=false} - end - - --Disables interactable status of objects with GUID in list - for _, guid in ipairs(ref_noninteractable) do - local obj = getObjectFromGUID(guid) - if obj then obj.interactable = false end - end - - - - obj_surface = getObjectFromGUID("721ba2") - - - controlActive = false - createOpenCloseButton() -end - - - ---Activation/deactivation of control panel - - - ---Activated by clicking on -function click_toggleControl(_, color) - if permissionCheck(color) then - if not controlActive then - --Activate control panel - controlActive = true - self.clearButtons() - createOpenCloseButton() - createSurfaceInput() - createSurfaceButtons() - - else - --Deactivate control panel - controlActive = false - self.clearButtons() - self.clearInputs() - createOpenCloseButton() - - end - end -end - - - - ---Table surface control - - - ---Changes table surface -function click_applySurface(_, color) - if permissionCheck(color) then - updateSurface() - broadcastToAll("New Playmat Image Applied", {0.2,0.9,0.2}) - end -end - ---Updates surface from the values in the input field -function updateSurface() - local customInfo = obj_surface.getCustomObject() - customInfo.image = self.getInputs()[1].value - obj_surface.setCustomObject(customInfo) - obj_surface = obj_surface.reload() -end - - - ---Information gathering - - - ---Checks if a color is promoted or host -function permissionCheck(color) - if Player[color].host==true or Player[color].promoted==true then - return true - else - return false - end -end - ---Locates a string saved within memory file -function findInImageDataIndex(...) - for _, str in ipairs({...}) do - for i, v in ipairs(tableImageData) do - if v.url == str or v.name == str then - return i - end - end - end - return nil -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 - ---Locates a button with a helper function -function findButton(obj, func) - if func==nil then error("No func supplied to findButton") end - for _, v in ipairs(obj.getButtons()) do - if func(v) then - return v - end - end - return nil -end - - - ---Creation of buttons/inputs - - - -function createOpenCloseButton() - local tooltip = "Open Playmat Panel" - if controlActive then - tooltip = "Close Playmat Panel" - end - self.createButton({ - click_function="click_toggleControl", function_owner=self, - position={0,0,0}, rotation={-45,0,0}, height=1500, width=1500, - color={1,1,1,0}, tooltip=tooltip - }) -end - -function createSurfaceInput() - local currentURL = obj_surface.getCustomObject().diffuse - local nickname = "" - if findInImageDataIndex(currentURL) ~= nil then - nickname = tableImageData[findInImageDataIndex(currentURL)].name - end - - self.createInput({ - label="URL", input_function="none", function_owner=self, - alignment=3, position={0,0.15,3}, height=224, width=4000, - font_size=200, tooltip="Enter URL for playmat image", - value=currentURL - }) -end - -function createSurfaceButtons() - --Label - self.createButton({ - label="Playmat Image Swapper", click_function="none", - position={0,0.15,2.2}, height=0, width=0, font_size=300, font_color={1,1,1} - }) - --Functional - self.createButton({ - label="Apply Image\nTo Playmat", click_function="click_applySurface", - function_owner=self, tooltip="Apply URL as playmat image", - position={0,0.15,4}, height=440, width=1400, font_size=200, - }) - -end - - - - - - ---Data tables - - - - -ref_noninteractable = { - "afc863","c8edca","393bf7","12c65e","f938a2","9f95fd","35b95f", - "5af8f2","4ee1f2","bd69bd" +-- Playmat Image Swapper +-- updated by: Chr1Z +-- original by: - +-- description: changes the big playmats image +information = { + version = "1.1", + last_updated = "10.10.2022" } -ref_playerColor = { - "White", "Brown", "Red", "Orange", "Yellow", - "Green", "Teal", "Blue", "Purple", "Pink", "Black" -} +defaultURL = "http://cloud-3.steamusercontent.com/ugc/998015670465071049/FFAE162920D67CF38045EFBD3B85AD0F916147B2/" ---Dummy function, absorbs unwanted triggers +-- parameters for open/close button for reusing +BUTTON_PARAMETERS = {} +BUTTON_PARAMETERS.function_owner = self +BUTTON_PARAMETERS.click_function = "click_toggleControl" +BUTTON_PARAMETERS.height = 1500 +BUTTON_PARAMETERS.width = 1500 +BUTTON_PARAMETERS.color = { 1, 1, 1, 0 } + +function onload() + controlActive = false + createOpenCloseButton() + + self.addContextMenuItem("More Information", function() + printToAll("------------------------------", "White") + printToAll("Playmat Image Swapper v" .. information["version"] .. " by Chr1Z", "Orange") + printToAll("last updated: " .. information["last_updated"], "White") + printToAll("Original made by unknown", "White") + end) +end + +-- click function for main button +function click_toggleControl() + self.clearButtons() + self.clearInputs() + + controlActive = not controlActive + createOpenCloseButton() + + if not controlActive then return end + + -- creates the label, input box and apply button + self.createButton({ + function_owner = self, + label = "Playmat Image Swapper", + tooltip = "", + click_function = "none", + position = { 0, 0.15, 2.2 }, + height = 0, + width = 0, + font_size = 300, + font_color = { 1, 1, 1 } + }) + + self.createInput({ + function_owner = self, + label = "URL", + tooltip = "Enter URL for playmat image", + input_function = "none", + alignment = 3, + position = { 0, 0.15, 3 }, + height = 323, + width = 4000, + font_size = 300 + }) + + self.createButton({ + function_owner = self, + label = "Apply Image\nTo Playmat", + tooltip = "Left-Click: Apply URL\nRight-Click: Reset to default image", + click_function = "click_applySurface", + position = { 0, 0.15, 4.1 }, + height = 460, + width = 1400, + font_size = 200 + }) +end + +-- click function for apply button +function click_applySurface(_, _, isRightClick) + if isRightClick then + updateSurface(defaultURL) + else + updateSurface(self.getInputs()[1].value) + end +end + +-- input function for the input box function none() end + +-- main function (can be called by other objects) +function updateSurface(newURL) + local obj_surface = getObjectFromGUID("721ba2") + local customInfo = obj_surface.getCustomObject() + + if newURL ~= "" and newURL ~= nil and newURL ~= defaultURL then + customInfo.image = newURL + broadcastToAll("New Playmat Image Applied", { 0.2, 0.9, 0.2 }) + else + customInfo.image = defaultURL + broadcastToAll("Default Playmat Image Applied", { 0.2, 0.9, 0.2 }) + end + + obj_surface.setCustomObject(customInfo) + obj_surface = obj_surface.reload() +end + +-- creates the main button +function createOpenCloseButton() + if controlActive then + BUTTON_PARAMETERS.tooltip = "Close Playmat Panel" + else + BUTTON_PARAMETERS.tooltip = "Open Playmat Panel" + end + self.createButton(BUTTON_PARAMETERS) +end diff --git a/src/playercards/AllCardsBag.ttslua b/src/playercards/AllCardsBag.ttslua index eb578943..8fcf8c81 100644 --- a/src/playercards/AllCardsBag.ttslua +++ b/src/playercards/AllCardsBag.ttslua @@ -1,4 +1,9 @@ +-- Position to check for weaknesses. Everything with X and Z less +-- than these values (down and right on the table) will be checked +local WEAKNESS_CHECK_X = 15 +local WEAKNESS_CHECK_Z = 37 + local cardIdIndex = { } local classAndLevelIndex = { } local basicWeaknessList = { } @@ -114,15 +119,18 @@ function buildSupplementalIndexes() for cardId, card in pairs(cardIdIndex) do local cardData = card.data local cardMetadata = card.metadata - -- Add card to the basic weakness list, if appropriate. Some weaknesses have - -- multiple copies, and are added multiple times - if (cardMetadata.weakness and cardMetadata.basicWeaknessCount ~= nil) then - for i = 1, cardMetadata.basicWeaknessCount do - table.insert(basicWeaknessList, cardMetadata.id) + -- 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 + if (cardId == cardMetadata.id) then + -- Add card to the basic weakness list, if appropriate. Some weaknesses have + -- multiple copies, and are added multiple times + if (cardMetadata.weakness and cardMetadata.basicWeaknessCount ~= nil) then + for i = 1, cardMetadata.basicWeaknessCount do + table.insert(basicWeaknessList, cardMetadata.id) end end - -- Add the card to the appropriate class and level indexes + -- Add the card to the appropriate class and level indexes local isGuardian = false local isSeeker = false local isMystic = false @@ -130,35 +138,38 @@ function buildSupplementalIndexes() local isSurvivor = false local isNeutral = false local upgradeKey + -- Excludes signature cards (which have no class or level) and alternate + -- ID entries if (cardMetadata.class ~= nil and cardMetadata.level ~= nil) then - isGuardian = string.match(cardMetadata.class, "Guardian") - isSeeker = string.match(cardMetadata.class, "Seeker") - isMystic = string.match(cardMetadata.class, "Mystic") - isRogue = string.match(cardMetadata.class, "Rogue") - isSurvivor = string.match(cardMetadata.class, "Survivor") - isNeutral = string.match(cardMetadata.class, "Neutral") - if (cardMetadata.level > 0) then - upgradeKey = "-upgrade" - else - upgradeKey = "-level0" - end - if (isGuardian) then - table.insert(classAndLevelIndex["Guardian"..upgradeKey], cardMetadata.id) - end - if (isSeeker) then - table.insert(classAndLevelIndex["Seeker"..upgradeKey], cardMetadata.id) - end - if (isMystic) then - table.insert(classAndLevelIndex["Mystic"..upgradeKey], cardMetadata.id) - end - if (isRogue) then - table.insert(classAndLevelIndex["Rogue"..upgradeKey], cardMetadata.id) - end - if (isSurvivor) then - table.insert(classAndLevelIndex["Survivor"..upgradeKey], cardMetadata.id) - end - if (isNeutral) then - table.insert(classAndLevelIndex["Neutral"..upgradeKey], cardMetadata.id) + isGuardian = string.match(cardMetadata.class, "Guardian") + isSeeker = string.match(cardMetadata.class, "Seeker") + isMystic = string.match(cardMetadata.class, "Mystic") + isRogue = string.match(cardMetadata.class, "Rogue") + isSurvivor = string.match(cardMetadata.class, "Survivor") + isNeutral = string.match(cardMetadata.class, "Neutral") + if (cardMetadata.level > 0) then + upgradeKey = "-upgrade" + else + upgradeKey = "-level0" + end + if (isGuardian) then + table.insert(classAndLevelIndex["Guardian"..upgradeKey], cardMetadata.id) + end + if (isSeeker) then + table.insert(classAndLevelIndex["Seeker"..upgradeKey], cardMetadata.id) + end + if (isMystic) then + table.insert(classAndLevelIndex["Mystic"..upgradeKey], cardMetadata.id) + end + if (isRogue) then + table.insert(classAndLevelIndex["Rogue"..upgradeKey], cardMetadata.id) + end + if (isSurvivor) then + table.insert(classAndLevelIndex["Survivor"..upgradeKey], cardMetadata.id) + end + if (isNeutral) then + table.insert(classAndLevelIndex["Neutral"..upgradeKey], cardMetadata.id) + end end end end @@ -172,6 +183,7 @@ end function cardComparator(id1, id2) local card1 = cardIdIndex[id1] local card2 = cardIdIndex[id2] + if (card1.metadata.level ~= card2.metadata.level) then return card1.metadata.level < card2.metadata.level end @@ -222,19 +234,92 @@ function getCardsByClassAndLevel(params) return classAndLevelIndex[params.class..upgradeKey]; end +-- Searches the bag for cards which match the given name and returns a list. Note that this is +-- an O(n) search without index support. It may be slow. +-- Parameter array must contain these fields to define the search: +-- name String or string fragment to search for names +-- exact Whether the name match should be exact +function getCardsByName(params) + local name = params.name + local exact = params.exact + local results = { } + -- Track cards (by ID) that we've added to avoid duplicates that may come from alternate IDs + local addedCards = { } + for _, cardData in pairs(cardIdIndex) do + if (not addedCards[cardData.metadata.id]) then + if (exact and (string.lower(cardData.data.Nickname) == string.lower(name))) + or (not exact and string.find(string.lower(cardData.data.Nickname), string.lower(name), 1, true)) then + table.insert(results, cardData) + addedCards[cardData.metadata.id] = true + end + end + end + return results +end + -- Gets a random basic weakness from the bag. Once a given ID has been returned -- it will be removed from the list and cannot be selected again until a reload -- occurs or the indexes are rebuilt, which will refresh the list to include all -- weaknesses. -- Return: String ID of the selected weakness. function getRandomWeaknessId() - local pickedIndex = math.random(#basicWeaknessList) - local weaknessId = basicWeaknessList[pickedIndex] - if (#basicWeaknessList > 1) then - table.remove(basicWeaknessList, pickedIndex) - else - broadcastToAll("All weaknesses have been drawn!", {0.9, 0.2, 0.2}) + local availableWeaknesses = buildAvailableWeaknesses() + if (#availableWeaknesses > 0) then + return availableWeaknesses[math.random(#availableWeaknesses)] + end +end + +-- Constructs a list of available basic weaknesses by starting with the full pool of basic +-- weaknesses then removing any which are currently in the play or deck construction areas +-- Return: Table array of weakness IDs which are valid to choose from +function buildAvailableWeaknesses() + local weaknessesInPlay = { } + local allObjects = getAllObjects() + for _, object in ipairs(allObjects) do + if (object.name == "Deck" and isInPlayArea(object)) then + for _, cardData in ipairs(object.getData().ContainedObjects) do + local cardMetadata = JSON.decode(cardData.GMNotes) + incrementWeaknessCount(weaknessesInPlay, cardMetadata) + end + elseif (object.name == "Card" and isInPlayArea(object)) then + local cardMetadata = JSON.decode(object.getGMNotes()) + incrementWeaknessCount(weaknessesInPlay, cardMetadata) + end end - return weaknessId + local availableWeaknesses = { } + for _, weaknessId in ipairs(basicWeaknessList) do + if (weaknessesInPlay[weaknessId] ~= nil and weaknessesInPlay[weaknessId] > 0) then + weaknessesInPlay[weaknessId] = weaknessesInPlay[weaknessId] - 1 + else + table.insert(availableWeaknesses, weaknessId) + end + end + return availableWeaknesses +end + +-- Helper function that adds one to the table entry for the number of weaknesses in play +function incrementWeaknessCount(table, cardMetadata) + if (isBasicWeakness(cardMetadata)) then + if (table[cardMetadata.id] == nil) then + table[cardMetadata.id] = 1 + else + table[cardMetadata.id] = table[cardMetadata.id] + 1 + end + end +end + +function isInPlayArea(object) + if (object == nil) then + return false + end + local position = object.getPosition() + return position.x < WEAKNESS_CHECK_X + and position.z < WEAKNESS_CHECK_Z +end +function isBasicWeakness(cardMetadata) + return cardMetadata ~= nil + and cardMetadata.weakness + and cardMetadata.basicWeaknessCount ~= nil + and cardMetadata.basicWeaknessCount > 0 end diff --git a/src/playercards/CardSearch.ttslua b/src/playercards/CardSearch.ttslua new file mode 100644 index 00000000..15a5647e --- /dev/null +++ b/src/playercards/CardSearch.ttslua @@ -0,0 +1,136 @@ +-- Search-A-Card +-- made by: Chr1Z +-- description: spawns the card with the specified name +information = { + version = "1.1", + last_updated = "10.10.2022" +} + +local BUTTON_PARAMETERS = {} +BUTTON_PARAMETERS.function_owner = self +BUTTON_PARAMETERS.height = 200 +BUTTON_PARAMETERS.width = 1200 +BUTTON_PARAMETERS.font_size = 75 + +-- save selected options +function onSave() return JSON.encode({ spawnAll, searchExact }) end + +function onLoad(saved_data) + -- loading saved data + local loaded_data = JSON.decode(saved_data) + spawnAll = loaded_data[1] or false + searchExact = loaded_data[2] or false + + allCardsBag = getObjectFromGUID("15bb07") + INPUT_BOX = "" + + self.createInput({ + input_function = "input_func", + function_owner = self, + label = "Click to enter card name", + alignment = 2, + position = { x = 0, y = 0.05, z = -1.6 }, + width = 1200, + height = 130, + font_size = 107 + }) + + -- index 0: button for spawn mode + BUTTON_PARAMETERS.click_function = "search" + BUTTON_PARAMETERS.label = "Spawn matching card(s)!" + BUTTON_PARAMETERS.position = { x = 0, y = 0.05, z = 1.15 } + self.createButton(BUTTON_PARAMETERS) + + -- index 1: button for spawn mode + if spawnAll then + BUTTON_PARAMETERS.label = "Mode: Spawn all matching cards " + else + BUTTON_PARAMETERS.label = "Mode: Spawn first matching card" + end + + BUTTON_PARAMETERS.click_function = "spawnMode" + BUTTON_PARAMETERS.position.z = 1.55 + self.createButton(BUTTON_PARAMETERS) + + -- index 2: button for search mode + if searchExact then + BUTTON_PARAMETERS.label = "Mode: Name matches search term" + else + BUTTON_PARAMETERS.label = "Mode: Name contains search term" + end + + BUTTON_PARAMETERS.click_function = "searchMode" + BUTTON_PARAMETERS.position.z = 1.95 + self.createButton(BUTTON_PARAMETERS) + + self.addContextMenuItem("More Information", function() + printToAll("------------------------------", "White") + printToAll("Search-A-Card v" .. information["version"] .. " by Chr1Z", "Orange") + printToAll("last updated: " .. information["last_updated"], "White") + end) +end + +-- main function +function search() + if INPUT_BOX == nil or string.len(INPUT_BOX) == 0 then + printToAll("Please enter a search string.", "Yellow") + return + end + + if string.len(INPUT_BOX) < 4 then + printToAll("Please enter a longer search string.", "Yellow") + return + end + + if allCardsBag == nil then + printToAll("Player card bag couldn't be found.", "Red") + return + end + + local cardList = allCardsBag.call("getCardsByName", { name = INPUT_BOX, exact = searchExact }) + if cardList == nil or #cardList == 0 then + printToAll("No match found.", "Red") + return + end + + -- search all objects in bag + local spawnCount = 0 + for i, card in ipairs(cardList) do + local pos = self.positionToWorld(Vector(0, 0.5 + spawnCount * 0.15, -0.225)) + local rot = self.getRotation() + spawnObjectData({ + data = card.data, + position = pos, + rotation = rot, + }) + if not spawnAll then + return + end + end +end + +function input_func(_, _, input, stillEditing) + if not stillEditing then INPUT_BOX = input end +end + +-- toggle spawn mode +function spawnMode() + spawnAll = not spawnAll + + if spawnAll then + self.editButton({ index = 1, label = "Mode: Spawn all matching cards " }) + else + self.editButton({ index = 1, label = "Mode: Spawn first matching card" }) + end +end + +-- toggle search mode +function searchMode() + searchExact = not searchExact + + if searchExact then + self.editButton({ index = 2, label = "Mode: Name matches search term" }) + else + self.editButton({ index = 2, label = "Mode: Name contains search term" }) + end +end diff --git a/src/playercards/RandomWeaknessGenerator.ttslua b/src/playercards/RandomWeaknessGenerator.ttslua index f083210d..8ac17439 100644 --- a/src/playercards/RandomWeaknessGenerator.ttslua +++ b/src/playercards/RandomWeaknessGenerator.ttslua @@ -16,6 +16,10 @@ end function buttonClick_draw() local allCardsBag = getObjectFromGUID(allCardsBagGuid) local weaknessId = allCardsBag.call("getRandomWeaknessId") + if (weaknessId == nil) then + broadcastToAll("All basic weaknesses are in play!", {0.9, 0.2, 0.2}) + return + end local card = allCardsBag.call("getCardById", { id = weaknessId }) spawnObjectData({ data = card.data, diff --git a/src/playermat/PlaymatRed.ttslua b/src/playermat/Playmat.ttslua similarity index 62% rename from src/playermat/PlaymatRed.ttslua rename to src/playermat/Playmat.ttslua index bef792bf..3d835d44 100644 --- a/src/playermat/PlaymatRed.ttslua +++ b/src/playermat/Playmat.ttslua @@ -1,34 +1,4 @@ --- set true to enable debug logging -DEBUG = false --- we use this to turn off collision handling (for clue spawning) --- until after load is complete (probably a better way to do this) -COLLISION_ENABLED = false --- position offsets, adjust these to reposition things relative to mat [x,y,z] -DRAWN_ENCOUNTER_CARD_OFFSET = {0.98, 0.5, -0.635} -DRAWN_CHAOS_TOKEN_OFFSET = {-1.2, 0.5, -0.45} -DISCARD_BUTTON_OFFSETS = { - {-0.98, 0.2, -0.945}, - {-0.525, 0.2, -0.945}, - {-0.07, 0.2, -0.945}, - {0.39, 0.2, -0.945}, - {0.84, 0.2, -0.945}, -} --- draw deck and discard zone -DECK_POSITION = { x=-1.4, y=0, z=0.3 } -DECK_ZONE_SCALE = { x=3, y=5, z=8 } -DRAW_DECK_POSITION = { x=-18.9, y=2.5, z=-26.7 } - --- play zone -PLAYER_COLOR = "Red" -PLAY_ZONE_POSITION = { x=-25, y=4, z=-27 } -PLAY_ZONE_ROTATION = { x=0, y=180, z=0 } -PLAY_ZONE_SCALE = { x=30, y=5, z=15 } - -RESOURCE_COUNTER_GUID = "a4b60d" - --- the position of the global discard pile --- TODO: delegate to global for any auto discard actions -DISCARD_POSITION = {-3.85, 3, 10.38} +local activeInvestigatorId = nil function log(message) if DEBUG then @@ -66,6 +36,7 @@ function makeDiscardButton(position, searchPosition, discardPosition, number) end function onload(save_state) + self.interactable = DEBUG DATA_HELPER = getObjectFromGUID('708279') PLAYER_CARDS = DATA_HELPER.getTable('PLAYER_CARD_DATA') @@ -73,15 +44,16 @@ function onload(save_state) -- positions of encounter card slots local encounterSlots = { - {1, 0, -0.7}, - {0.55, 0, -0.7}, - {0.1, 0, -0.7}, - {-0.35, 0, -0.7}, - {-0.8, 0, -0.7} + {1.365, 0, -0.7}, + {0.91, 0, -0.7}, + {0.455, 0, -0.7}, + {0, 0, -0.7}, + {-0.455, 0, -0.7}, + {-0.91, 0, -0.7}, } local i = 1 - while i <= 5 do + while i <= 6 do makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], encounterSlots[i], DISCARD_POSITION, i) i = i + 1 end @@ -90,7 +62,7 @@ function onload(save_state) label = " ", click_function = "drawEncountercard", function_owner = self, - position = {-1.45,0,-0.7}, + position = {-1.88,0,-0.7}, rotation = {0,-15,0}, width = 170, height = 255, @@ -101,7 +73,7 @@ function onload(save_state) label=" ", click_function = "drawChaostokenButton", function_owner = self, - position = {1.48,0.0,-0.74}, + position = {1.84,0.0,-0.74}, rotation = {0,-45,0}, width = 125, height = 125, @@ -112,24 +84,13 @@ function onload(save_state) label="Upkeep", click_function = "doUpkeep", function_owner = self, - position = {1.48,0.1,-0.44}, + position = {1.84,0.1,-0.44}, scale = {0.12, 0.12, 0.12}, width = 800, height = 280, font_size = 180 }) - -- self.createButton({ - -- label="Draw 1", - -- click_function = "doDrawOne", - -- function_owner = self, - -- position = {1.48,0.1,-0.36}, - -- scale = {0.12, 0.12, 0.12}, - -- width = 800, - -- height = 280, - -- font_size = 180 - -- }) - local state = JSON.decode(save_state) if state ~= nil then if state.playerColor ~= nil then @@ -157,34 +118,31 @@ function setMessageColor(color) messageColor = Player[PLAYER_COLOR].seated and PLAYER_COLOR or color end -function getDrawDiscardDecks(zone) - -- get the draw deck and discard pile objects +-- get the draw deck and discard pile objects +function getDrawDiscardDecks() drawDeck = nil discardPile = nil + topCard = nil + + local zone = getObjectFromGUID(zoneID) + if zone == nil then return end + for i,object in ipairs(zone.getObjects()) do if object.tag == "Deck" or object.tag == "Card" then - if object.is_face_down then - drawDeck = object - else + local relativePos = self.positionToLocal(object.getPosition()) + if relativePos.z > 0.5 then discardPile = object + else + if investigator == "Norman Withers" and object.tag == "Card" and not object.is_face_down then + topCard = object + else + drawDeck = object + end end end end end -function checkDeckThenDrawOne() - -- draw 1 card, shuffling the discard pile if necessary - if drawDeck == nil then - if discardPile ~= nil then - shuffleDiscardIntoDeck() - Wait.time(|| drawCards(1), 1) - end - printToColor("Take 1 horror (drawing card from empty deck)", messageColor) - else - drawCards(1) - end -end - function doUpkeep(obj, color, alt_click) -- right-click binds to new player color if alt_click then @@ -206,70 +164,93 @@ function doUpkeep(obj, color, alt_click) local y = PLAY_ZONE_ROTATION.y - local investigator = nil + investigator = nil + local miniId = nil + local forcedLearning = false for i,v in ipairs(objs) do local obj = v.hit_object - local props = obj.getCustomObject() + local props = obj.getCustomObject() or {} if obj.tag == "Card" and not obj.is_face_down and not doNotReady(obj) then - if props ~= nil and props.unique_back then - local name = obj.getName() + local notes = JSON.decode(obj.getGMNotes()) or {} + local name = obj.getName() + if notes.type == "Investigator" and notes.id ~= nil then + miniId = string.match(notes.id, "%d%d%d%d%d%d-") .. "-m" + end + if notes.type == "Investigator" or props.unique_back then if string.match(name, "Jenny Barnes") ~= nil then investigator = "Jenny Barnes" elseif name == "Patrice Hathaway" then investigator = name + elseif string.match(name, "Norman Withers") ~= nil then + investigator = "Norman Withers" end + elseif name == "Forced Learning" then + forcedLearning = true else local r = obj.getRotation() if (r.y - y > 10) or (y - r.y > 10) then obj.setRotation(PLAY_ZONE_ROTATION) end end - elseif obj.tag == "Board" and obj.getDescription() == "Action token" then + elseif obj.getDescription() == "Action Token" then if obj.is_face_down then obj.flip() end end end + -- flip investigator mini-card if found + if miniId ~= nil then + objs = getObjects() + for i,obj in ipairs(objs) do + if obj.tag == "Card" then + local notes = JSON.decode(obj.getGMNotes()) + if notes ~= nil and notes.type == "Minicard" and notes.id == miniId then + if obj.is_face_down then + obj.flip() + end + goto done + end + end + end + end + ::done:: + + -- flip summoned servitor mini-cards (To-Do: don't flip all of them) + for i, obj in ipairs(getObjects()) do + if obj.tag == "Card" then + local notes = JSON.decode(obj.getGMNotes()) + if notes ~= nil and notes.type == "Minicard" and notes.id == "09080-m" then + if obj.is_face_down then + obj.flip() + end + end + end + end + -- gain resource getObjectFromGUID(RESOURCE_COUNTER_GUID).call("add_subtract") if investigator == "Jenny Barnes" then getObjectFromGUID(RESOURCE_COUNTER_GUID).call("add_subtract") - printToColor("Taking 2 resources (Jenny)", messageColor) + printToColor("Gaining 2 resources (Jenny)", messageColor) end - -- get the draw deck and discard pile objects - local zone = getObjectFromGUID(zoneID) - if zone == nil then return end - - getDrawDiscardDecks(zone) - -- special draw for Patrice Hathaway (shuffle discards if necessary) if investigator == "Patrice Hathaway" then patriceDraw() return end - -- draw 1 card (shuffle discards if necessary) - checkDeckThenDrawOne() -end - -function doDrawOne(obj, color, alt_click) - -- right-click binds to new player color - if alt_click then - PLAYER_COLOR = color - printToColor("Draw 1 button bound to " .. color, color) + -- special draw for Forced Learning + if forcedLearning then + forcedLearningDraw() return end + drawCardsWithReshuffle(1) +end + +function doDrawOne(obj, color) setMessageColor(color) - - -- get the draw deck and discard pile objects - local zone = getObjectFromGUID(zoneID) - if zone == nil then return end - - getDrawDiscardDecks(zone) - - -- draw 1 card (shuffle discards if necessary) - checkDeckThenDrawOne() + drawCardsWithReshuffle(1) end function doNotReady(card) @@ -280,13 +261,67 @@ function doNotReady(card) end end +-- draw X cards (shuffle discards if necessary) +function drawCardsWithReshuffle(numCards) + if type(numCards) ~= "number" then numCards = 1 end + + getDrawDiscardDecks() + + if investigator == "Norman Withers" then + local harbinger = false + if topCard ~= nil and topCard.getName() == "The Harbinger" then + harbinger = true + else + if drawDeck ~= nil and not drawDeck.is_face_down then + local cards = drawDeck.getObjects() + local bottomCard = cards[#cards] + if bottomCard.name == "The Harbinger" then + harbinger = true + end + end + end + + if harbinger then + printToColor("The Harbinger is on top of your deck, not drawing cards", messageColor) + return -1 + end + + if topCard ~= nil then + topCard.deal(numCards, PLAYER_COLOR) + numCards = numCards - 1 + if numCards == 0 then return end + end + end + + local deckSize + if drawDeck == nil then + deckSize = 0 + elseif drawDeck.tag == "Deck" then + deckSize = #drawDeck.getObjects() + else + deckSize = 1 + end + + if deckSize >= numCards then + drawCards(numCards) + return + end + + drawCards(deckSize) + if discardPile ~= nil then + shuffleDiscardIntoDeck() + Wait.time(|| drawCards(numCards - deckSize), 1) + end + printToColor("Take 1 horror (drawing card from empty deck)", messageColor) +end + function drawCards(numCards) if drawDeck == nil then return end drawDeck.deal(numCards, PLAYER_COLOR) end function shuffleDiscardIntoDeck() - discardPile.flip() + if not discardPile.is_face_down then discardPile.flip() end discardPile.shuffle() discardPile.setPositionSmooth(DRAW_DECK_POSITION, false, false) drawDeck = discardPile @@ -297,27 +332,13 @@ function patriceDraw() local handSize = #Player[PLAYER_COLOR].getHandObjects() if handSize >= 5 then return end local cardsToDraw = 5 - handSize - local deckSize printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor) - if drawDeck == nil then - deckSize = 0 - elseif drawDeck.tag == "Deck" then - deckSize = #drawDeck.getObjects() - else - deckSize = 1 - end + drawCardsWithReshuffle(cardsToDraw) +end - if deckSize >= cardsToDraw then - drawCards(cardsToDraw) - return - end - - drawCards(deckSize) - if discardPile ~= nil then - shuffleDiscardIntoDeck() - Wait.time(|| drawCards(cardsToDraw - deckSize), 1) - end - printToColor("Take 1 horror (drawing card from empty deck)", messageColor) +function forcedLearningDraw() + printToColor("Drawing 2 cards, discard 1 (Forced Learning)", messageColor) + drawCardsWithReshuffle(2) end function checkDeckZoneExists() @@ -368,6 +389,9 @@ end -- spawn a group of tokens of the given type on the object function spawnTokenGroup(object, tokenType, tokenCount) + if (tokenCount < 1 or tokenCount > 12) then + return + end local offsets = PLAYER_CARD_TOKEN_OFFSETS[tokenCount] if offsets == nil then error("couldn't find offsets for " .. tokenCount .. ' tokens') @@ -392,8 +416,10 @@ function shouldSpawnTokens(object) -- we assume we shouldn't spawn tokens if in doubt, this should -- only ever happen on load and in that case prevents respawns local spawned = DATA_HELPER.call('getSpawnedPlayerCardGuid', {object.getGUID()}) - local canSpawn = getPlayerCardData(object) - return not spawned and canSpawn + local hasDataHelperData = getPlayerCardData(object) + local cardMetadata = JSON.decode(object.getGMNotes()) or {} + local hasUses = cardMetadata.uses ~= nil + return not spawned and (hasDataHelperData or hasUses) end function markSpawned(object) @@ -404,12 +430,32 @@ function markSpawned(object) end function spawnTokensFor(object) - local data = getPlayerCardData(object) - if data == nil then - error('attempt to spawn tokens for ' .. object.getName() .. ': no token data') + local cardMetadata = JSON.decode(object.getGMNotes()) or {} + local token = nil + local type = nil + local tokenCount = 0 + if (cardMetadata.uses ~= nil) then + for i, useInfo in ipairs(cardMetadata.uses) do + token = useInfo.token + type = useInfo.type + tokenCount = useInfo.count + if (activeInvestigatorId == "03004" and useInfo.type == "Charge") then + tokenCount = tokenCount + 1 + end + log("Spawning tokens for "..object.getName()..'['..object.getDescription()..']: '..tokenCount.."x "..token) + spawnTokenGroup(object, token, tokenCount) + end + else + local data = getPlayerCardData(object) + if data == nil then + error('attempt to spawn tokens for ' .. object.getName() .. ': no token data') + end + token = data['tokenType'] + tokenCount = data['tokenCount'] + log(object.getName() .. '[' .. object.getDescription() .. ']' .. ' : ' .. data['tokenType'] .. ' : ' .. data['tokenCount']) + log("Spawning tokens for "..object.getName()..'['..object.getDescription()..']: '..tokenCount.."x "..token) + spawnTokenGroup(object, token, tokenCount) end - log(object.getName() .. '[' .. object.getDescription() .. ']' .. ' : ' .. data['tokenType'] .. ' : ' .. data['tokenCount']) - spawnTokenGroup(object, data['tokenType'], data['tokenCount']) markSpawned(object) end @@ -438,6 +484,9 @@ function unmarkSpawned(guid, force) end function onCollisionEnter(collision_info) + if (collision_info.collision_object.name == "Card") then + maybeUpdateActiveInvestigator(collision_info.collision_object) + end if not COLLISION_ENABLED then return end @@ -456,6 +505,28 @@ function onCollisionEnter(collision_info) end end +function maybeUpdateActiveInvestigator(card) + local cardMetadata = JSON.decode(card.getGMNotes()) or {} + if (cardMetadata.type == "Investigator") then + activeInvestigatorId = cardMetadata.id + updateStatToken(willpowerTokenGuid, cardMetadata.willpowerIcons) + updateStatToken(intellectTokenGuid, cardMetadata.intellectIcons) + updateStatToken(combatTokenGuid, cardMetadata.combatIcons) + updateStatToken(agilityTokenGuid, cardMetadata.agilityIcons) + end +end + +function updateStatToken(tokenGuid, val) + local statToken = getObjectFromGUID(tokenGuid) + if (statToken == nil) then + return + end + statToken.call("reset_val") + for i = 1, val do + statToken.call("add_subtract", { alt_click = false }) + end +end + -- functions delegated to Global function drawChaostokenButton(object, player, isRightClick) -- local toPosition = self.positionToWorld(DRAWN_CHAOS_TOKEN_OFFSET) diff --git a/src/playermat/PlaymatGreen.ttslua b/src/playermat/PlaymatGreen.ttslua deleted file mode 100644 index bee11ca3..00000000 --- a/src/playermat/PlaymatGreen.ttslua +++ /dev/null @@ -1,480 +0,0 @@ --- set true to enable debug logging -DEBUG = false --- we use this to turn off collision handling (for clue spawning) --- until after load is complete (probably a better way to do this) -COLLISION_ENABLED = false --- position offsets, adjust these to reposition things relative to mat [x,y,z] -DRAWN_ENCOUNTER_CARD_OFFSET = {0.98, 0.5, -0.635} -DRAWN_CHAOS_TOKEN_OFFSET = {-1.2, 0.5, -0.45} -DISCARD_BUTTON_OFFSETS = { - {-0.98, 0.2, -0.945}, - {-0.525, 0.2, -0.945}, - {-0.07, 0.2, -0.945}, - {0.39, 0.2, -0.945}, - {0.84, 0.2, -0.945}, -} --- draw deck and discard zone -DECK_POSITION = { x=-1.4, y=0, z=0.3 } -DECK_ZONE_SCALE = { x=3, y=5, z=8 } -DRAW_DECK_POSITION = { x=-37, y=2.5, z=26.5 } - --- play zone -PLAYER_COLOR = "Green" -PLAY_ZONE_POSITION = { x=-25, y=4, z=27 } -PLAY_ZONE_ROTATION = { x=0, y=0, z=0 } -PLAY_ZONE_SCALE = { x=30, y=5, z=15 } - -RESOURCE_COUNTER_GUID = "cd15ac" - --- the position of the global discard pile --- TODO: delegate to global for any auto discard actions -DISCARD_POSITION = {-3.85, 3, 10.38} - -function log(message) - if DEBUG then - print(message) - end -end - --- builds a function that discards things in searchPostion to discardPostition -function makeDiscardHandlerFor(searchPosition, discardPosition) - return function (_) - local discardItemList = findObjectsAtPosition(searchPosition) - for _, obj in ipairs(discardItemList) do - obj.setPositionSmooth(discardPosition, false, true) - obj.setRotation({0, -90, 0}) - end - end -end - --- build a discard button at position to discard from searchPosition to discardPosition --- number must be unique -function makeDiscardButton(position, searchPosition, discardPosition, number) - local handler = makeDiscardHandlerFor(searchPosition, discardPosition) - local handlerName = 'handler' .. number - self.setVar(handlerName, handler) - self.createButton({ - label = "Discard", - click_function= handlerName, - function_owner= self, - position = position, - scale = {0.12, 0.12, 0.12}, - width = 800, - height = 280, - font_size = 180, - }) -end - -function onload(save_state) - self.interactable = DEBUG - DATA_HELPER = getObjectFromGUID('708279') - PLAYER_CARDS = DATA_HELPER.getTable('PLAYER_CARD_DATA') - PLAYER_CARD_TOKEN_OFFSETS = DATA_HELPER.getTable('PLAYER_CARD_TOKEN_OFFSETS') - - -- positions of encounter card slots - local encounterSlots = { - {1, 0, -0.7}, - {0.55, 0, -0.7}, - {0.1, 0, -0.7}, - {-0.35, 0, -0.7}, - {-0.8, 0, -0.7} - } - - local i = 1 - while i <= 5 do - makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], encounterSlots[i], DISCARD_POSITION, i) - i = i + 1 - end - - self.createButton({ - label = " ", - click_function = "drawEncountercard", - function_owner = self, - position = {-1.45,0,-0.7}, - rotation = {0,-15,0}, - width = 170, - height = 255, - font_size = 50 - }) - - self.createButton({ - label=" ", - click_function = "drawChaostokenButton", - function_owner = self, - position = {1.48,0.0,-0.74}, - rotation = {0,-45,0}, - width = 125, - height = 125, - font_size = 50 - }) - - self.createButton({ - label="Upkeep", - click_function = "doUpkeep", - function_owner = self, - position = {1.48,0.1,-0.44}, - scale = {0.12, 0.12, 0.12}, - width = 800, - height = 280, - font_size = 180 - }) - - -- self.createButton({ - -- label="Draw 1", - -- click_function = "doDrawOne", - -- function_owner = self, - -- position = {1.48,0.1,-0.36}, - -- scale = {0.12, 0.12, 0.12}, - -- width = 800, - -- height = 280, - -- font_size = 180 - -- }) - - local state = JSON.decode(save_state) - if state ~= nil then - if state.playerColor ~= nil then - PLAYER_COLOR = state.playerColor - end - if state.zoneID ~= nil then - zoneID = state.zoneID - Wait.time(checkDeckZoneExists, 30) - else - spawnDeckZone() - end - else - spawnDeckZone() - end - - COLLISION_ENABLED = true -end - -function onSave() - return JSON.encode({ zoneID=zoneID, playerColor=PLAYER_COLOR }) -end - -function setMessageColor(color) - -- send messages to player who clicked button if no seated player found - messageColor = Player[PLAYER_COLOR].seated and PLAYER_COLOR or color -end - -function getDrawDiscardDecks(zone) - -- get the draw deck and discard pile objects - drawDeck = nil - discardPile = nil - for i,object in ipairs(zone.getObjects()) do - if object.tag == "Deck" or object.tag == "Card" then - if object.is_face_down then - drawDeck = object - else - discardPile = object - end - end - end -end - -function checkDeckThenDrawOne() - -- draw 1 card, shuffling the discard pile if necessary - if drawDeck == nil then - if discardPile ~= nil then - shuffleDiscardIntoDeck() - Wait.time(|| drawCards(1), 1) - end - printToColor("Take 1 horror (drawing card from empty deck)", messageColor) - else - drawCards(1) - end -end - -function doUpkeep(obj, color, alt_click) - -- right-click binds to new player color - if alt_click then - PLAYER_COLOR = color - printToColor("Upkeep button bound to " .. color, color) - return - end - - setMessageColor(color) - - -- unexhaust cards in play zone - local objs = Physics.cast({ - origin = PLAY_ZONE_POSITION, - direction = { x=0, y=1, z=0 }, - type = 3, - size = PLAY_ZONE_SCALE, - orientation = PLAY_ZONE_ROTATION - }) - - local y = PLAY_ZONE_ROTATION.y - - local investigator = nil - for i,v in ipairs(objs) do - local obj = v.hit_object - local props = obj.getCustomObject() - if obj.tag == "Card" and not obj.is_face_down and not doNotReady(obj) then - if props ~= nil and props.unique_back then - local name = obj.getName() - if string.match(name, "Jenny Barnes") ~= nil then - investigator = "Jenny Barnes" - elseif name == "Patrice Hathaway" then - investigator = name - end - else - local r = obj.getRotation() - if (r.y - y > 10) or (y - r.y > 10) then - obj.setRotation(PLAY_ZONE_ROTATION) - end - end - elseif obj.tag == "Board" and obj.getDescription() == "Action token" then - if obj.is_face_down then obj.flip() end - end - end - - -- gain resource - getObjectFromGUID(RESOURCE_COUNTER_GUID).call("add_subtract") - if investigator == "Jenny Barnes" then - getObjectFromGUID(RESOURCE_COUNTER_GUID).call("add_subtract") - printToColor("Taking 2 resources (Jenny)", messageColor) - end - - -- get the draw deck and discard pile objects - local zone = getObjectFromGUID(zoneID) - if zone == nil then return end - - getDrawDiscardDecks(zone) - - -- special draw for Patrice Hathaway (shuffle discards if necessary) - if investigator == "Patrice Hathaway" then - patriceDraw() - return - end - - -- draw 1 card (shuffle discards if necessary) - checkDeckThenDrawOne() -end - -function doDrawOne(obj, color, alt_click) - -- right-click binds to new player color - if alt_click then - PLAYER_COLOR = color - printToColor("Draw 1 button bound to " .. color, color) - return - end - - setMessageColor(color) - - -- get the draw deck and discard pile objects - local zone = getObjectFromGUID(zoneID) - if zone == nil then return end - - getDrawDiscardDecks(zone) - - -- draw 1 card (shuffle discards if necessary) - checkDeckThenDrawOne() -end - -function doNotReady(card) - if card.getVar("do_not_ready") == true then - return true - else - return false - end -end - -function drawCards(numCards) - if drawDeck == nil then return end - drawDeck.deal(numCards, PLAYER_COLOR) -end - -function shuffleDiscardIntoDeck() - discardPile.flip() - discardPile.shuffle() - discardPile.setPositionSmooth(DRAW_DECK_POSITION, false, false) - drawDeck = discardPile - discardPile = nil -end - -function patriceDraw() - local handSize = #Player[PLAYER_COLOR].getHandObjects() - if handSize >= 5 then return end - local cardsToDraw = 5 - handSize - local deckSize - printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor) - if drawDeck == nil then - deckSize = 0 - elseif drawDeck.tag == "Deck" then - deckSize = #drawDeck.getObjects() - else - deckSize = 1 - end - - if deckSize >= cardsToDraw then - drawCards(cardsToDraw) - return - end - - drawCards(deckSize) - if discardPile ~= nil then - shuffleDiscardIntoDeck() - Wait.time(|| drawCards(cardsToDraw - deckSize), 1) - end - printToColor("Take 1 horror (drawing card from empty deck)", messageColor) -end - -function checkDeckZoneExists() - if getObjectFromGUID(zoneID) ~= nil then return end - spawnDeckZone() -end - -function spawnDeckZone() - local pos = self.positionToWorld(DECK_POSITION) - local zoneProps = { - position = pos, - scale = DECK_ZONE_SCALE, - type = 'ScriptingTrigger', - callback = 'zoneCallback', - callback_owner = self, - rotation = self.getRotation() - } - spawnObject(zoneProps) -end - -function zoneCallback(zone) - zoneID = zone.getGUID() -end - -function findObjectsAtPosition(localPos) - local globalPos = self.positionToWorld(localPos) - local objList = Physics.cast({ - origin=globalPos, --Where the cast takes place - direction={0,1,0}, --Which direction it moves (up is shown) - type=2, --Type. 2 is "sphere" - size={2,2,2}, --How large that sphere is - max_distance=1, --How far it moves. Just a little bit - debug=false --If it displays the sphere when casting. - }) - local decksAndCards = {} - for _, obj in ipairs(objList) do - if obj.hit_object.tag == "Deck" or obj.hit_object.tag == "Card" then - table.insert(decksAndCards, obj.hit_object) - end - end - return decksAndCards -end - -function spawnTokenOn(object, offsets, tokenType) - local tokenPosition = object.positionToWorld(offsets) - spawnToken(tokenPosition, tokenType) -end - --- spawn a group of tokens of the given type on the object -function spawnTokenGroup(object, tokenType, tokenCount) - local offsets = PLAYER_CARD_TOKEN_OFFSETS[tokenCount] - if offsets == nil then - error("couldn't find offsets for " .. tokenCount .. ' tokens') - end - local i = 0 - while i < tokenCount do - local offset = offsets[i + 1] - spawnTokenOn(object, offset, tokenType) - i = i + 1 - end -end - -function buildPlayerCardKey(object) - return object.getName() .. ':' .. object.getDescription() -end - -function getPlayerCardData(object) - return PLAYER_CARDS[buildPlayerCardKey(object)] or PLAYER_CARDS[object.getName()] -end - -function shouldSpawnTokens(object) - -- we assume we shouldn't spawn tokens if in doubt, this should - -- only ever happen on load and in that case prevents respawns - local spawned = DATA_HELPER.call('getSpawnedPlayerCardGuid', {object.getGUID()}) - local canSpawn = getPlayerCardData(object) - return not spawned and canSpawn -end - -function markSpawned(object) - local saved = DATA_HELPER.call('setSpawnedPlayerCardGuid', {object.getGUID(), true}) - if not saved then - error('attempt to mark player card spawned before data loaded') - end -end - -function spawnTokensFor(object) - local data = getPlayerCardData(object) - if data == nil then - error('attempt to spawn tokens for ' .. object.getName() .. ': no token data') - end - log(object.getName() .. '[' .. object.getDescription() .. ']' .. ' : ' .. data['tokenType'] .. ' : ' .. data['tokenCount']) - spawnTokenGroup(object, data['tokenType'], data['tokenCount']) - markSpawned(object) -end - -function resetSpawnState() - local zone = getObjectFromGUID(zoneID) - if zone == nil then return end - - for i,object in ipairs(zone.getObjects()) do - if object.tag == "Card" then - local guid = object.getGUID() - if guid ~= nil then unmarkSpawned(guid, true) end - elseif object.tag == "Deck" then - local cards = object.getObjects() - if (cards ~= nil) then - for i,v in ipairs(cards) do - if v.guid ~= nil then unmarkSpawned(v.guid) end - end - end - end - end -end - -function unmarkSpawned(guid, force) - if not force and getObjectFromGUID(guid) ~= nil then return end - DATA_HELPER.call('setSpawnedPlayerCardGuid', {guid, false}) -end - -function onCollisionEnter(collision_info) - if not COLLISION_ENABLED then - return - end - - local object = collision_info.collision_object - Wait.time(resetSpawnState, 1) - -- anything to the left of this is legal to spawn - local discardSpawnBoundary = self.positionToWorld({-1.2, 0, 0}) - local boundaryLocalToCard = object.positionToLocal(discardSpawnBoundary) - if boundaryLocalToCard.x > 0 then - log('not checking for token spawn, boundary relative is ' .. boundaryLocalToCard.x) - return - end - if not object.is_face_down and shouldSpawnTokens(object) then - spawnTokensFor(object) - end -end - --- functions delegated to Global -function drawChaostokenButton(object, player, isRightClick) - -- local toPosition = self.positionToWorld(DRAWN_CHAOS_TOKEN_OFFSET) - Global.call("drawChaostoken", {self, DRAWN_CHAOS_TOKEN_OFFSET, isRightClick}) -end - -function drawEncountercard(object, player, isRightClick) -local toPosition = self.positionToWorld(DRAWN_ENCOUNTER_CARD_OFFSET) -Global.call("drawEncountercard", {toPosition, self.getRotation(), isRightClick}) -end - -function spawnToken(position, tokenType) - Global.call('spawnToken', {position, tokenType}) -end - -function updatePlayerCards(args) - local custom_data_helper = getObjectFromGUID(args[1]) - data_player_cards = custom_data_helper.getTable("PLAYER_CARD_DATA") - for k, v in pairs(data_player_cards) do - PLAYER_CARDS[k] = v - end -end diff --git a/src/playermat/PlaymatOrange.ttslua b/src/playermat/PlaymatOrange.ttslua deleted file mode 100644 index d3e58430..00000000 --- a/src/playermat/PlaymatOrange.ttslua +++ /dev/null @@ -1,480 +0,0 @@ --- set true to enable debug logging -DEBUG = false --- we use this to turn off collision handling (for clue spawning) --- until after load is complete (probably a better way to do this) -COLLISION_ENABLED = false --- position offsets, adjust these to reposition things relative to mat [x,y,z] -DRAWN_ENCOUNTER_CARD_OFFSET = {0.98, 0.5, -0.635} -DRAWN_CHAOS_TOKEN_OFFSET = {-1.2, 0.5, -0.45} -DISCARD_BUTTON_OFFSETS = { - {-0.98, 0.2, -0.945}, - {-0.525, 0.2, -0.945}, - {-0.07, 0.2, -0.945}, - {0.39, 0.2, -0.945}, - {0.84, 0.2, -0.945}, -} --- draw deck and discard zone -DECK_POSITION = { x=-1.4, y=0, z=0.3 } -DECK_ZONE_SCALE = { x=3, y=5, z=8 } -DRAW_DECK_POSITION = { x=-55, y=2.5, z=-22.7 } - --- play zone -PLAYER_COLOR = "Orange" -PLAY_ZONE_POSITION = { x=-54.53, y=4.10, z=-20.94} -PLAY_ZONE_ROTATION = { x=0, y=270, z=0 } -PLAY_ZONE_SCALE = { x=36.96, y=5.10, z=14.70} - -RESOURCE_COUNTER_GUID = "816d84" - --- the position of the global discard pile --- TODO: delegate to global for any auto discard actions -DISCARD_POSITION = {-3.85, 3, 10.38} - -function log(message) - if DEBUG then - print(message) - end -end - --- builds a function that discards things in searchPostion to discardPostition -function makeDiscardHandlerFor(searchPosition, discardPosition) - return function (_) - local discardItemList = findObjectsAtPosition(searchPosition) - for _, obj in ipairs(discardItemList) do - obj.setPositionSmooth(discardPosition, false, true) - obj.setRotation({0, -90, 0}) - end - end -end - --- build a discard button at position to discard from searchPosition to discardPosition --- number must be unique -function makeDiscardButton(position, searchPosition, discardPosition, number) - local handler = makeDiscardHandlerFor(searchPosition, discardPosition) - local handlerName = 'handler' .. number - self.setVar(handlerName, handler) - self.createButton({ - label = "Discard", - click_function= handlerName, - function_owner= self, - position = position, - scale = {0.12, 0.12, 0.12}, - width = 800, - height = 280, - font_size = 180, - }) -end - -function onload(save_state) - self.interactable = DEBUG - DATA_HELPER = getObjectFromGUID('708279') - PLAYER_CARDS = DATA_HELPER.getTable('PLAYER_CARD_DATA') - PLAYER_CARD_TOKEN_OFFSETS = DATA_HELPER.getTable('PLAYER_CARD_TOKEN_OFFSETS') - - -- positions of encounter card slots - local encounterSlots = { - {1, 0, -0.7}, - {0.55, 0, -0.7}, - {0.1, 0, -0.7}, - {-0.35, 0, -0.7}, - {-0.8, 0, -0.7} - } - - local i = 1 - while i <= 5 do - makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], encounterSlots[i], DISCARD_POSITION, i) - i = i + 1 - end - - self.createButton({ - label = " ", - click_function = "drawEncountercard", - function_owner = self, - position = {-1.45,0,-0.7}, - rotation = {0,-15,0}, - width = 170, - height = 255, - font_size = 50 - }) - - self.createButton({ - label=" ", - click_function = "drawChaostokenButton", - function_owner = self, - position = {1.48,0.0,-0.74}, - rotation = {0,-45,0}, - width = 125, - height = 125, - font_size = 50 - }) - - self.createButton({ - label="Upkeep", - click_function = "doUpkeep", - function_owner = self, - position = {1.48,0.1,-0.44}, - scale = {0.12, 0.12, 0.12}, - width = 800, - height = 280, - font_size = 180 - }) - - -- self.createButton({ - -- label="Draw 1", - -- click_function = "doDrawOne", - -- function_owner = self, - -- position = {1.48,0.1,-0.36}, - -- scale = {0.12, 0.12, 0.12}, - -- width = 800, - -- height = 280, - -- font_size = 180 - -- }) - - local state = JSON.decode(save_state) - if state ~= nil then - if state.playerColor ~= nil then - PLAYER_COLOR = state.playerColor - end - if state.zoneID ~= nil then - zoneID = state.zoneID - Wait.time(checkDeckZoneExists, 30) - else - spawnDeckZone() - end - else - spawnDeckZone() - end - - COLLISION_ENABLED = true -end - -function onSave() - return JSON.encode({ zoneID=zoneID, playerColor=PLAYER_COLOR }) -end - -function setMessageColor(color) - -- send messages to player who clicked button if no seated player found - messageColor = Player[PLAYER_COLOR].seated and PLAYER_COLOR or color -end - -function getDrawDiscardDecks(zone) - -- get the draw deck and discard pile objects - drawDeck = nil - discardPile = nil - for i,object in ipairs(zone.getObjects()) do - if object.tag == "Deck" or object.tag == "Card" then - if object.is_face_down then - drawDeck = object - else - discardPile = object - end - end - end -end - -function checkDeckThenDrawOne() - -- draw 1 card, shuffling the discard pile if necessary - if drawDeck == nil then - if discardPile ~= nil then - shuffleDiscardIntoDeck() - Wait.time(|| drawCards(1), 1) - end - printToColor("Take 1 horror (drawing card from empty deck)", messageColor) - else - drawCards(1) - end -end - -function doUpkeep(obj, color, alt_click) - -- right-click binds to new player color - if alt_click then - PLAYER_COLOR = color - printToColor("Upkeep button bound to " .. color, color) - return - end - - setMessageColor(color) - - -- unexhaust cards in play zone - local objs = Physics.cast({ - origin = PLAY_ZONE_POSITION, - direction = { x=0, y=1, z=0 }, - type = 3, - size = PLAY_ZONE_SCALE, - orientation = PLAY_ZONE_ROTATION - }) - - local y = PLAY_ZONE_ROTATION.y - - local investigator = nil - for i,v in ipairs(objs) do - local obj = v.hit_object - local props = obj.getCustomObject() - if obj.tag == "Card" and not obj.is_face_down and not doNotReady(obj) then - if props ~= nil and props.unique_back then - local name = obj.getName() - if string.match(name, "Jenny Barnes") ~= nil then - investigator = "Jenny Barnes" - elseif name == "Patrice Hathaway" then - investigator = name - end - else - local r = obj.getRotation() - if (r.y - y > 10) or (y - r.y > 10) then - obj.setRotation(PLAY_ZONE_ROTATION) - end - end - elseif obj.tag == "Board" and obj.getDescription() == "Action token" then - if obj.is_face_down then obj.flip() end - end - end - - -- gain resource - getObjectFromGUID(RESOURCE_COUNTER_GUID).call("add_subtract") - if investigator == "Jenny Barnes" then - getObjectFromGUID(RESOURCE_COUNTER_GUID).call("add_subtract") - printToColor("Taking 2 resources (Jenny)", messageColor) - end - - -- get the draw deck and discard pile objects - local zone = getObjectFromGUID(zoneID) - if zone == nil then return end - - getDrawDiscardDecks(zone) - - -- special draw for Patrice Hathaway (shuffle discards if necessary) - if investigator == "Patrice Hathaway" then - patriceDraw() - return - end - - -- draw 1 card (shuffle discards if necessary) - checkDeckThenDrawOne() -end - -function doDrawOne(obj, color, alt_click) - -- right-click binds to new player color - if alt_click then - PLAYER_COLOR = color - printToColor("Draw 1 button bound to " .. color, color) - return - end - - setMessageColor(color) - - -- get the draw deck and discard pile objects - local zone = getObjectFromGUID(zoneID) - if zone == nil then return end - - getDrawDiscardDecks(zone) - - -- draw 1 card (shuffle discards if necessary) - checkDeckThenDrawOne() -end - -function doNotReady(card) - if card.getVar("do_not_ready") == true then - return true - else - return false - end -end - -function drawCards(numCards) - if drawDeck == nil then return end - drawDeck.deal(numCards, PLAYER_COLOR) -end - -function shuffleDiscardIntoDeck() - discardPile.flip() - discardPile.shuffle() - discardPile.setPositionSmooth(DRAW_DECK_POSITION, false, false) - drawDeck = discardPile - discardPile = nil -end - -function patriceDraw() - local handSize = #Player[PLAYER_COLOR].getHandObjects() - if handSize >= 5 then return end - local cardsToDraw = 5 - handSize - local deckSize - printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor) - if drawDeck == nil then - deckSize = 0 - elseif drawDeck.tag == "Deck" then - deckSize = #drawDeck.getObjects() - else - deckSize = 1 - end - - if deckSize >= cardsToDraw then - drawCards(cardsToDraw) - return - end - - drawCards(deckSize) - if discardPile ~= nil then - shuffleDiscardIntoDeck() - Wait.time(|| drawCards(cardsToDraw - deckSize), 1) - end - printToColor("Take 1 horror (drawing card from empty deck)", messageColor) -end - -function checkDeckZoneExists() - if getObjectFromGUID(zoneID) ~= nil then return end - spawnDeckZone() -end - -function spawnDeckZone() - local pos = self.positionToWorld(DECK_POSITION) - local zoneProps = { - position = pos, - scale = DECK_ZONE_SCALE, - type = 'ScriptingTrigger', - callback = 'zoneCallback', - callback_owner = self, - rotation = self.getRotation() - } - spawnObject(zoneProps) -end - -function zoneCallback(zone) - zoneID = zone.getGUID() -end - -function findObjectsAtPosition(localPos) - local globalPos = self.positionToWorld(localPos) - local objList = Physics.cast({ - origin=globalPos, --Where the cast takes place - direction={0,1,0}, --Which direction it moves (up is shown) - type=2, --Type. 2 is "sphere" - size={2,2,2}, --How large that sphere is - max_distance=1, --How far it moves. Just a little bit - debug=false --If it displays the sphere when casting. - }) - local decksAndCards = {} - for _, obj in ipairs(objList) do - if obj.hit_object.tag == "Deck" or obj.hit_object.tag == "Card" then - table.insert(decksAndCards, obj.hit_object) - end - end - return decksAndCards -end - -function spawnTokenOn(object, offsets, tokenType) - local tokenPosition = object.positionToWorld(offsets) - spawnToken(tokenPosition, tokenType) -end - --- spawn a group of tokens of the given type on the object -function spawnTokenGroup(object, tokenType, tokenCount) - local offsets = PLAYER_CARD_TOKEN_OFFSETS[tokenCount] - if offsets == nil then - error("couldn't find offsets for " .. tokenCount .. ' tokens') - end - local i = 0 - while i < tokenCount do - local offset = offsets[i + 1] - spawnTokenOn(object, offset, tokenType) - i = i + 1 - end -end - -function buildPlayerCardKey(object) - return object.getName() .. ':' .. object.getDescription() -end - -function getPlayerCardData(object) - return PLAYER_CARDS[buildPlayerCardKey(object)] or PLAYER_CARDS[object.getName()] -end - -function shouldSpawnTokens(object) - -- we assume we shouldn't spawn tokens if in doubt, this should - -- only ever happen on load and in that case prevents respawns - local spawned = DATA_HELPER.call('getSpawnedPlayerCardGuid', {object.getGUID()}) - local canSpawn = getPlayerCardData(object) - return not spawned and canSpawn -end - -function markSpawned(object) - local saved = DATA_HELPER.call('setSpawnedPlayerCardGuid', {object.getGUID(), true}) - if not saved then - error('attempt to mark player card spawned before data loaded') - end -end - -function spawnTokensFor(object) - local data = getPlayerCardData(object) - if data == nil then - error('attempt to spawn tokens for ' .. object.getName() .. ': no token data') - end - log(object.getName() .. '[' .. object.getDescription() .. ']' .. ' : ' .. data['tokenType'] .. ' : ' .. data['tokenCount']) - spawnTokenGroup(object, data['tokenType'], data['tokenCount']) - markSpawned(object) -end - -function resetSpawnState() - local zone = getObjectFromGUID(zoneID) - if zone == nil then return end - - for i,object in ipairs(zone.getObjects()) do - if object.tag == "Card" then - local guid = object.getGUID() - if guid ~= nil then unmarkSpawned(guid, true) end - elseif object.tag == "Deck" then - local cards = object.getObjects() - if (cards ~= nil) then - for i,v in ipairs(cards) do - if v.guid ~= nil then unmarkSpawned(v.guid) end - end - end - end - end -end - -function unmarkSpawned(guid, force) - if not force and getObjectFromGUID(guid) ~= nil then return end - DATA_HELPER.call('setSpawnedPlayerCardGuid', {guid, false}) -end - -function onCollisionEnter(collision_info) - if not COLLISION_ENABLED then - return - end - - local object = collision_info.collision_object - Wait.time(resetSpawnState, 1) - -- anything to the left of this is legal to spawn - local discardSpawnBoundary = self.positionToWorld({-1.2, 0, 0}) - local boundaryLocalToCard = object.positionToLocal(discardSpawnBoundary) - if boundaryLocalToCard.x > 0 then - log('not checking for token spawn, boundary relative is ' .. boundaryLocalToCard.x) - return - end - if not object.is_face_down and shouldSpawnTokens(object) then - spawnTokensFor(object) - end -end - --- functions delegated to Global -function drawChaostokenButton(object, player, isRightClick) - -- local toPosition = self.positionToWorld(DRAWN_CHAOS_TOKEN_OFFSET) - Global.call("drawChaostoken", {self, DRAWN_CHAOS_TOKEN_OFFSET, isRightClick}) -end - -function drawEncountercard(object, player, isRightClick) -local toPosition = self.positionToWorld(DRAWN_ENCOUNTER_CARD_OFFSET) -Global.call("drawEncountercard", {toPosition, self.getRotation(), isRightClick}) -end - -function spawnToken(position, tokenType) - Global.call('spawnToken', {position, tokenType}) -end - -function updatePlayerCards(args) - local custom_data_helper = getObjectFromGUID(args[1]) - data_player_cards = custom_data_helper.getTable("PLAYER_CARD_DATA") - for k, v in pairs(data_player_cards) do - PLAYER_CARDS[k] = v - end -end diff --git a/src/playermat/PlaymatWhite.ttslua b/src/playermat/PlaymatWhite.ttslua deleted file mode 100644 index 4f93ed6f..00000000 --- a/src/playermat/PlaymatWhite.ttslua +++ /dev/null @@ -1,480 +0,0 @@ --- set true to enable debug logging -DEBUG = false --- we use this to turn off collision handling (for clue spawning) --- until after load is complete (probably a better way to do this) -COLLISION_ENABLED = false --- position offsets, adjust these to reposition things relative to mat [x,y,z] -DRAWN_ENCOUNTER_CARD_OFFSET = {0.98, 0.5, -0.635} -DRAWN_CHAOS_TOKEN_OFFSET = {-1.2, 0.5, -0.45} -DISCARD_BUTTON_OFFSETS = { - {-0.98, 0.2, -0.945}, - {-0.525, 0.2, -0.945}, - {-0.07, 0.2, -0.945}, - {0.39, 0.2, -0.945}, - {0.84, 0.2, -0.945}, -} --- draw deck and discard zone -DECK_POSITION = { x=-1.4, y=0, z=0.3 } -DECK_ZONE_SCALE = { x=3, y=5, z=8 } -DRAW_DECK_POSITION = { x=-55, y=2.5, z=4.5 } - --- play zone -PLAYER_COLOR = "White" -PLAY_ZONE_POSITION = { x=-54.42, y=4.10, z=20.96} -PLAY_ZONE_ROTATION = { x=0, y=270, z=0 } -PLAY_ZONE_SCALE = { x=36.63, y=5.10, z=14.59} - -RESOURCE_COUNTER_GUID = "4406f0" - --- the position of the global discard pile --- TODO: delegate to global for any auto discard actions -DISCARD_POSITION = {-3.85, 3, 10.38} - -function log(message) - if DEBUG then - print(message) - end -end - --- builds a function that discards things in searchPostion to discardPostition -function makeDiscardHandlerFor(searchPosition, discardPosition) - return function (_) - local discardItemList = findObjectsAtPosition(searchPosition) - for _, obj in ipairs(discardItemList) do - obj.setPositionSmooth(discardPosition, false, true) - obj.setRotation({0, -90, 0}) - end - end -end - --- build a discard button at position to discard from searchPosition to discardPosition --- number must be unique -function makeDiscardButton(position, searchPosition, discardPosition, number) - local handler = makeDiscardHandlerFor(searchPosition, discardPosition) - local handlerName = 'handler' .. number - self.setVar(handlerName, handler) - self.createButton({ - label = "Discard", - click_function= handlerName, - function_owner= self, - position = position, - scale = {0.12, 0.12, 0.12}, - width = 800, - height = 280, - font_size = 180, - }) -end - -function onload(save_state) - self.interactable = DEBUG - DATA_HELPER = getObjectFromGUID('708279') - PLAYER_CARDS = DATA_HELPER.getTable('PLAYER_CARD_DATA') - PLAYER_CARD_TOKEN_OFFSETS = DATA_HELPER.getTable('PLAYER_CARD_TOKEN_OFFSETS') - - -- positions of encounter card slots - local encounterSlots = { - {1, 0, -0.7}, - {0.55, 0, -0.7}, - {0.1, 0, -0.7}, - {-0.35, 0, -0.7}, - {-0.8, 0, -0.7} - } - - local i = 1 - while i <= 5 do - makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], encounterSlots[i], DISCARD_POSITION, i) - i = i + 1 - end - - self.createButton({ - label = " ", - click_function = "drawEncountercard", - function_owner = self, - position = {-1.45,0,-0.7}, - rotation = {0,-15,0}, - width = 170, - height = 255, - font_size = 50 - }) - - self.createButton({ - label=" ", - click_function = "drawChaostokenButton", - function_owner = self, - position = {1.48,0.0,-0.74}, - rotation = {0,-45,0}, - width = 125, - height = 125, - font_size = 50 - }) - - self.createButton({ - label="Upkeep", - click_function = "doUpkeep", - function_owner = self, - position = {1.48,0.1,-0.44}, - scale = {0.12, 0.12, 0.12}, - width = 800, - height = 280, - font_size = 180 - }) - - -- self.createButton({ - -- label="Draw 1", - -- click_function = "doDrawOne", - -- function_owner = self, - -- position = {1.48,0.1,-0.36}, - -- scale = {0.12, 0.12, 0.12}, - -- width = 800, - -- height = 280, - -- font_size = 180 - -- }) - - local state = JSON.decode(save_state) - if state ~= nil then - if state.playerColor ~= nil then - PLAYER_COLOR = state.playerColor - end - if state.zoneID ~= nil then - zoneID = state.zoneID - Wait.time(checkDeckZoneExists, 30) - else - spawnDeckZone() - end - else - spawnDeckZone() - end - - COLLISION_ENABLED = true -end - -function onSave() - return JSON.encode({ zoneID=zoneID, playerColor=PLAYER_COLOR }) -end - -function setMessageColor(color) - -- send messages to player who clicked button if no seated player found - messageColor = Player[PLAYER_COLOR].seated and PLAYER_COLOR or color -end - -function getDrawDiscardDecks(zone) - -- get the draw deck and discard pile objects - drawDeck = nil - discardPile = nil - for i,object in ipairs(zone.getObjects()) do - if object.tag == "Deck" or object.tag == "Card" then - if object.is_face_down then - drawDeck = object - else - discardPile = object - end - end - end -end - -function checkDeckThenDrawOne() - -- draw 1 card, shuffling the discard pile if necessary - if drawDeck == nil then - if discardPile ~= nil then - shuffleDiscardIntoDeck() - Wait.time(|| drawCards(1), 1) - end - printToColor("Take 1 horror (drawing card from empty deck)", messageColor) - else - drawCards(1) - end -end - -function doUpkeep(obj, color, alt_click) - -- right-click binds to new player color - if alt_click then - PLAYER_COLOR = color - printToColor("Upkeep button bound to " .. color, color) - return - end - - setMessageColor(color) - - -- unexhaust cards in play zone - local objs = Physics.cast({ - origin = PLAY_ZONE_POSITION, - direction = { x=0, y=1, z=0 }, - type = 3, - size = PLAY_ZONE_SCALE, - orientation = PLAY_ZONE_ROTATION - }) - - local y = PLAY_ZONE_ROTATION.y - - local investigator = nil - for i,v in ipairs(objs) do - local obj = v.hit_object - local props = obj.getCustomObject() - if obj.tag == "Card" and not obj.is_face_down and not doNotReady(obj) then - if props ~= nil and props.unique_back then - local name = obj.getName() - if string.match(name, "Jenny Barnes") ~= nil then - investigator = "Jenny Barnes" - elseif name == "Patrice Hathaway" then - investigator = name - end - else - local r = obj.getRotation() - if (r.y - y > 10) or (y - r.y > 10) then - obj.setRotation(PLAY_ZONE_ROTATION) - end - end - elseif obj.tag == "Board" and obj.getDescription() == "Action token" then - if obj.is_face_down then obj.flip() end - end - end - - -- gain resource - getObjectFromGUID(RESOURCE_COUNTER_GUID).call("add_subtract") - if investigator == "Jenny Barnes" then - getObjectFromGUID(RESOURCE_COUNTER_GUID).call("add_subtract") - printToColor("Taking 2 resources (Jenny)", messageColor) - end - - -- get the draw deck and discard pile objects - local zone = getObjectFromGUID(zoneID) - if zone == nil then return end - - getDrawDiscardDecks(zone) - - -- special draw for Patrice Hathaway (shuffle discards if necessary) - if investigator == "Patrice Hathaway" then - patriceDraw() - return - end - - -- draw 1 card (shuffle discards if necessary) - checkDeckThenDrawOne() -end - -function doDrawOne(obj, color, alt_click) - -- right-click binds to new player color - if alt_click then - PLAYER_COLOR = color - printToColor("Draw 1 button bound to " .. color, color) - return - end - - setMessageColor(color) - - -- get the draw deck and discard pile objects - local zone = getObjectFromGUID(zoneID) - if zone == nil then return end - - getDrawDiscardDecks(zone) - - -- draw 1 card (shuffle discards if necessary) - checkDeckThenDrawOne() -end - -function doNotReady(card) - if card.getVar("do_not_ready") == true then - return true - else - return false - end -end - -function drawCards(numCards) - if drawDeck == nil then return end - drawDeck.deal(numCards, PLAYER_COLOR) -end - -function shuffleDiscardIntoDeck() - discardPile.flip() - discardPile.shuffle() - discardPile.setPositionSmooth(DRAW_DECK_POSITION, false, false) - drawDeck = discardPile - discardPile = nil -end - -function patriceDraw() - local handSize = #Player[PLAYER_COLOR].getHandObjects() - if handSize >= 5 then return end - local cardsToDraw = 5 - handSize - local deckSize - printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor) - if drawDeck == nil then - deckSize = 0 - elseif drawDeck.tag == "Deck" then - deckSize = #drawDeck.getObjects() - else - deckSize = 1 - end - - if deckSize >= cardsToDraw then - drawCards(cardsToDraw) - return - end - - drawCards(deckSize) - if discardPile ~= nil then - shuffleDiscardIntoDeck() - Wait.time(|| drawCards(cardsToDraw - deckSize), 1) - end - printToColor("Take 1 horror (drawing card from empty deck)", messageColor) -end - -function checkDeckZoneExists() - if getObjectFromGUID(zoneID) ~= nil then return end - spawnDeckZone() -end - -function spawnDeckZone() - local pos = self.positionToWorld(DECK_POSITION) - local zoneProps = { - position = pos, - scale = DECK_ZONE_SCALE, - type = 'ScriptingTrigger', - callback = 'zoneCallback', - callback_owner = self, - rotation = self.getRotation() - } - spawnObject(zoneProps) -end - -function zoneCallback(zone) - zoneID = zone.getGUID() -end - -function findObjectsAtPosition(localPos) - local globalPos = self.positionToWorld(localPos) - local objList = Physics.cast({ - origin=globalPos, --Where the cast takes place - direction={0,1,0}, --Which direction it moves (up is shown) - type=2, --Type. 2 is "sphere" - size={2,2,2}, --How large that sphere is - max_distance=1, --How far it moves. Just a little bit - debug=false --If it displays the sphere when casting. - }) - local decksAndCards = {} - for _, obj in ipairs(objList) do - if obj.hit_object.tag == "Deck" or obj.hit_object.tag == "Card" then - table.insert(decksAndCards, obj.hit_object) - end - end - return decksAndCards -end - -function spawnTokenOn(object, offsets, tokenType) - local tokenPosition = object.positionToWorld(offsets) - spawnToken(tokenPosition, tokenType) -end - --- spawn a group of tokens of the given type on the object -function spawnTokenGroup(object, tokenType, tokenCount) - local offsets = PLAYER_CARD_TOKEN_OFFSETS[tokenCount] - if offsets == nil then - error("couldn't find offsets for " .. tokenCount .. ' tokens') - end - local i = 0 - while i < tokenCount do - local offset = offsets[i + 1] - spawnTokenOn(object, offset, tokenType) - i = i + 1 - end -end - -function buildPlayerCardKey(object) - return object.getName() .. ':' .. object.getDescription() -end - -function getPlayerCardData(object) - return PLAYER_CARDS[buildPlayerCardKey(object)] or PLAYER_CARDS[object.getName()] -end - -function shouldSpawnTokens(object) - -- we assume we shouldn't spawn tokens if in doubt, this should - -- only ever happen on load and in that case prevents respawns - local spawned = DATA_HELPER.call('getSpawnedPlayerCardGuid', {object.getGUID()}) - local canSpawn = getPlayerCardData(object) - return not spawned and canSpawn -end - -function markSpawned(object) - local saved = DATA_HELPER.call('setSpawnedPlayerCardGuid', {object.getGUID(), true}) - if not saved then - error('attempt to mark player card spawned before data loaded') - end -end - -function spawnTokensFor(object) - local data = getPlayerCardData(object) - if data == nil then - error('attempt to spawn tokens for ' .. object.getName() .. ': no token data') - end - log(object.getName() .. '[' .. object.getDescription() .. ']' .. ' : ' .. data['tokenType'] .. ' : ' .. data['tokenCount']) - spawnTokenGroup(object, data['tokenType'], data['tokenCount']) - markSpawned(object) -end - -function resetSpawnState() - local zone = getObjectFromGUID(zoneID) - if zone == nil then return end - - for i,object in ipairs(zone.getObjects()) do - if object.tag == "Card" then - local guid = object.getGUID() - if guid ~= nil then unmarkSpawned(guid, true) end - elseif object.tag == "Deck" then - local cards = object.getObjects() - if (cards ~= nil) then - for i,v in ipairs(cards) do - if v.guid ~= nil then unmarkSpawned(v.guid) end - end - end - end - end -end - -function unmarkSpawned(guid, force) - if not force and getObjectFromGUID(guid) ~= nil then return end - DATA_HELPER.call('setSpawnedPlayerCardGuid', {guid, false}) -end - -function onCollisionEnter(collision_info) - if not COLLISION_ENABLED then - return - end - - local object = collision_info.collision_object - Wait.time(resetSpawnState, 1) - -- anything to the left of this is legal to spawn - local discardSpawnBoundary = self.positionToWorld({-1.2, 0, 0}) - local boundaryLocalToCard = object.positionToLocal(discardSpawnBoundary) - if boundaryLocalToCard.x > 0 then - log('not checking for token spawn, boundary relative is ' .. boundaryLocalToCard.x) - return - end - if not object.is_face_down and shouldSpawnTokens(object) then - spawnTokensFor(object) - end -end - --- functions delegated to Global -function drawChaostokenButton(object, player, isRightClick) - -- local toPosition = self.positionToWorld(DRAWN_CHAOS_TOKEN_OFFSET) - Global.call("drawChaostoken", {self, DRAWN_CHAOS_TOKEN_OFFSET, isRightClick}) -end - -function drawEncountercard(object, player, isRightClick) -local toPosition = self.positionToWorld(DRAWN_ENCOUNTER_CARD_OFFSET) -Global.call("drawEncountercard", {toPosition, self.getRotation(), isRightClick}) -end - -function spawnToken(position, tokenType) - Global.call('spawnToken', {position, tokenType}) -end - -function updatePlayerCards(args) - local custom_data_helper = getObjectFromGUID(args[1]) - data_player_cards = custom_data_helper.getTable("PLAYER_CARD_DATA") - for k, v in pairs(data_player_cards) do - PLAYER_CARDS[k] = v - end -end diff --git a/src/playermat/SkillToken.ttslua b/src/playermat/SkillToken.ttslua new file mode 100644 index 00000000..f5448079 --- /dev/null +++ b/src/playermat/SkillToken.ttslua @@ -0,0 +1,132 @@ +MIN_VALUE = -99 +MAX_VALUE = 999 + +function onload(saved_data) + light_mode = false + val = 0 + + if saved_data ~= "" then + local loaded_data = JSON.decode(saved_data) + light_mode = loaded_data[1] + val = loaded_data[2] + end + + createAll() +end + +function updateSave() + local data_to_save = {light_mode, val} + saved_data = JSON.encode(data_to_save) + self.script_state = saved_data +end + +function createAll() + s_color = {0.5, 0.5, 0.5, 95} + + if light_mode then + f_color = {1,1,1,95} + else + f_color = {0,0,0,100} + end + + + + self.createButton({ + label=tostring(val), + click_function="add_subtract", + function_owner=self, + position={0,0.05,0}, + height=600, + width=1000, + alignment = 3, + scale={x=1.5, y=1.5, z=1.5}, + font_size=600, + font_color=f_color, + color={0,0,0,0} + }) + + + + + if light_mode then + lightButtonText = "[ Set dark ]" + else + lightButtonText = "[ Set light ]" + end + +end + +function removeAll() + self.removeInput(0) + self.removeInput(1) + self.removeButton(0) + self.removeButton(1) + self.removeButton(2) +end + +function reloadAll() + removeAll() + createAll() + + updateSave() +end + +function swap_fcolor(_obj, _color, alt_click) + light_mode = not light_mode + reloadAll() +end + +function swap_align(_obj, _color, alt_click) + center_mode = not center_mode + reloadAll() +end + +function editName(_obj, _string, value) + self.setName(value) + setTooltips() +end + +function add_subtract(_obj, _color, alt_click) + mod = alt_click and -1 or 1 + new_value = math.min(math.max(val + mod, MIN_VALUE), MAX_VALUE) + if val ~= new_value then + val = new_value + updateVal() + updateSave() + end +end + +function updateVal() + + self.editButton({ + index = 0, + label = tostring(val), + + }) +end + +function reset_val() + val = 0 + updateVal() + updateSave() +end + +function setTooltips() + self.editInput({ + index = 0, + value = self.getName(), + tooltip = ttText + }) + self.editButton({ + index = 0, + value = tostring(val), + tooltip = ttText + }) +end + +function null() +end + +function keepSample(_obj, _string, value) + reloadAll() +end diff --git a/src/playermat/Zones.ttslua b/src/playermat/Zones.ttslua new file mode 100644 index 00000000..bd61fb54 --- /dev/null +++ b/src/playermat/Zones.ttslua @@ -0,0 +1,164 @@ +-- Sets up and returns coordinates for all possible spawn zones. Because Lua assigns tables by reference +-- and there is no built-in function to copy a table this is relatively brute force. +-- +-- Positions are all relative to the player mat, and most are consistent. The +-- exception are the SetAside# zones, which are placed to the left of the mat +-- for White/Green, and the right of the mat for Orange/Red. +-- +-- Investigator: Investigator card area. +-- Minicard: Placement for the investigator's minicard, just above the player mat +-- Deck, Discard: Standard locations for the deck and discard piles. +-- BlankTop: used for assets that start in play (e.g. Duke) +-- Tarot, Hand1, Hand2, Ally, BlankBottom, Accessory, Arcane1, Arcane2, Body: Asset slot positions +-- Threat[1-4]: Threat area slots. Threat[1-3] correspond to the named threat area slots, and Threat4 is the blank threat area slot. +-- SetAside[1-3]: Column closest to the player mat, with 1 at the top and 3 at the bottom. +-- SetAside[4-6]: Column farther away from the mat, with 4 at the top and 6 at the bottom. +-- SetAside1: Permanent cards +-- SetAside2: Bonded cards +-- SetAside3: Ancestral Knowledge / Underworld Market +-- SetAside4: Upgrade sheets for customizable cards +-- SetAside5: Hunch Deck for Joe Diamond +-- SetAside6: currently unused +do + + local playerMatGuids = {} + playerMatGuids["Red"] = "0840d5" + playerMatGuids["Orange"] = "bd0ff4" + playerMatGuids["White"] = "8b081b" + playerMatGuids["Green"] = "383d8b" + + commonZones = {} + commonZones["Investigator"] = { -1.17702, 0, 0.00209 } + commonZones["Minicard"] = { -0.4668214, 0, -1.222326 } + commonZones["Deck"] = { -1.822724, 0, -0.02940192 } + commonZones["Discard"] = { -1.822451, 0, 0.6092291 } + commonZones["Ally"] = { -0.6157398, 0, 0.02435675 } + commonZones["Body"] = { -0.6306521, 0, 0.553170 } + commonZones["Hand1"] = { 0.2155387, 0, 0.04257287 } + commonZones["Hand2"] = { -0.1803701, 0, 0.03745948 } + commonZones["Arcane1"] = { 0.2124223, 0, 0.5596902 } + commonZones["Arcane2"] = { -0.1711275, 0, 0.5567944 } + commonZones["Tarot"] = { 0.6016169, 0, 0.03273106 } + commonZones["Accessory"] = { 0.6049907, 0, 0.5546234 } + commonZones["BlankTop"] = { 1.758446, 0, 0.03965336 } + commonZones["BlankBottom"] = { 1.754469, 0, 0.5634764 } + commonZones["Threat1"] = { -0.9116555, 0, -0.6446251 } + commonZones["Threat2"] = { -0.4544126, 0, -0.6428719 } + commonZones["Threat3"] = { 0.002246313, 0, -0.6430681 } + commonZones["Threat4"] = { 0.4590618, 0, -0.6432732 } + + Zones = {} + Zones["White"] = {} + Zones["White"]["Investigator"] = commonZones["Investigator"] + Zones["White"]["Minicard"] = commonZones["Minicard"] + Zones["White"]["Deck"] = commonZones["Deck"] + Zones["White"]["Discard"] = commonZones["Discard"] + Zones["White"]["Ally"] = commonZones["Ally"] + Zones["White"]["Body"] = commonZones["Body"] + Zones["White"]["Hand1"] = commonZones["Hand1"] + Zones["White"]["Hand2"] = commonZones["Hand2"] + Zones["White"]["Arcane1"] = commonZones["Arcane1"] + Zones["White"]["Arcane2"] = commonZones["Arcane2"] + Zones["White"]["Tarot"] = commonZones["Tarot"] + Zones["White"]["Accessory"] = commonZones["Accessory"] + Zones["White"]["BlankTop"] = commonZones["BlankTop"] + Zones["White"]["BlankBottom"] = commonZones["BlankBottom"] + Zones["White"]["Threat1"] = commonZones["Threat1"] + Zones["White"]["Threat2"] = commonZones["Threat2"] + Zones["White"]["Threat3"] = commonZones["Threat3"] + Zones["White"]["Threat4"] = commonZones["Threat4"] + Zones["White"]["SetAside1"] = { 2.345893, 0, -0.520315 } + Zones["White"]["SetAside2"] = { 2.345893, 0, 0.042552 } + Zones["White"]["SetAside3"] = { 2.345893, 0, 0.605419 } + Zones["White"]["UnderSetAside3"] = { 2.495893, 0, 0.805419 } + Zones["White"]["SetAside4"] = { 2.775893, 0, -0.520315 } + Zones["White"]["SetAside5"] = { 2.775893, 0, 0.042552 } + Zones["White"]["SetAside6"] = { 2.775893, 0, 0.605419 } + Zones["White"]["UnderSetAside6"] = { 2.925893, 0, 0.805419 } + + Zones["Orange"] = {} + Zones["Orange"]["Investigator"] = commonZones["Investigator"] + Zones["Orange"]["Minicard"] = commonZones["Minicard"] + Zones["Orange"]["Deck"] = commonZones["Deck"] + Zones["Orange"]["Discard"] = commonZones["Discard"] + Zones["Orange"]["Ally"] = commonZones["Ally"] + Zones["Orange"]["Body"] = commonZones["Body"] + Zones["Orange"]["Hand1"] = commonZones["Hand1"] + Zones["Orange"]["Hand2"] = commonZones["Hand2"] + Zones["Orange"]["Arcane1"] = commonZones["Arcane1"] + Zones["Orange"]["Arcane2"] = commonZones["Arcane2"] + Zones["Orange"]["Tarot"] = commonZones["Tarot"] + Zones["Orange"]["Accessory"] = commonZones["Accessory"] + Zones["Orange"]["BlankTop"] = commonZones["BlankTop"] + Zones["Orange"]["BlankBottom"] = commonZones["BlankBottom"] + Zones["Orange"]["Threat1"] = commonZones["Threat1"] + Zones["Orange"]["Threat2"] = commonZones["Threat2"] + Zones["Orange"]["Threat3"] = commonZones["Threat3"] + Zones["Orange"]["Threat4"] = commonZones["Threat4"] + Zones["Orange"]["SetAside1"] = { -2.350362, 0, -0.520315 } + Zones["Orange"]["SetAside2"] = { -2.350362, 0, 0.042552 } + Zones["Orange"]["SetAside3"] = { -2.350362, 0, 0.605419 } + Zones["Orange"]["UnderSetAside3"] = { -2.500362, 0, 0.80419 } + Zones["Orange"]["SetAside4"] = { -2.7803627, 0, -0.520315 } + Zones["Orange"]["SetAside5"] = { -2.7803627, 0, 0.042552 } + Zones["Orange"]["SetAside6"] = { -2.7803627, 0, 0.605419 } + Zones["Orange"]["UnderSetAside6"] = { -2.9303627, 0, 0.80419 } + + -- Green positions are the same as White and Red the same as Orange + Zones["Red"] = Zones["Orange"] + Zones["Green"] = Zones["White"] + + -- 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 Zones.getDefaultCardZone(cardMetadata) + if cardMetadata.type == "Investigator" then + return "Investigator" + elseif cardMetadata.type == "Minicard" then + return "Minicard" + elseif cardMetadata.permanent then + return "SetAside1" + -- TODO: Figure out how to handled bonded information which isn't her now that we split the file + -- elseif bondedList[cardMetadata.id] then + -- return "SetAside2" + -- SetAside3 is used for Ancestral Knowledge / Underworld Market + -- SetAside4 is used for upgrade sheets + else + return "Deck" + end + end + + -- Gets the global position for the given zone on the specified player mat. + ---@param playerColor: Color name of the player mat to get the zone position for (e.g. "Red") + ---@param zoneName: Name of the zone to get the position for. See Zones object documentation for a list of valid zones. + ---@return: Global position table, or nil if an invalid player color or zone is specified + function Zones.getZonePosition(playerColor, zoneName) + if (playerColor ~= "Red" + and playerColor ~= "Orange" + and playerColor ~= "White" + and playerColor ~= "Green") then + return nil + end + return getObjectFromGUID(playerMatGuids[playerColor]).positionToWorld(Zones[playerColor][zoneName]) + end + + -- Return the global rotation for a card on the given player mat, based on its metadata. + ---@param playerColor: Color name of the player mat to get the rotation for (e.g. "Red") + ---@param cardMetadata: Table of card metadata. Metadata fields type and permanent are required; all others are optional. + ---@return: Global rotation vector for the given card. This will include the + -- Y rotation to orient the card on the given player mat as well as a + -- Z rotation to place the card face up or face down. + function Zones.getDefaultCardRotation(playerColor, zone) + local deckRotation = getObjectFromGUID(playerMatGuids[playerColor]).getRotation() + + if zone == "Investigator" then + deckRotation = deckRotation + Vector(0, 270, 0) + elseif zone == "Deck" then + deckRotation = deckRotation + Vector(0, 0, 180) + end + + return deckRotation + end + + return Zones +end diff --git a/src/tokens/BlessCurseManager.ttslua b/src/tokens/BlessCurseManager.ttslua index b8ad9578..d621112e 100644 --- a/src/tokens/BlessCurseManager.ttslua +++ b/src/tokens/BlessCurseManager.ttslua @@ -1,5 +1,5 @@ -BLESS_COLOR = { r=0.3, g=0.25, b=0.09 } -CURSE_COLOR = { r=0.2, g=0.08, b=0.24 } +BLESS_COLOR = { r = 0.3, g = 0.25, b = 0.09 } +CURSE_COLOR = { r = 0.2, g = 0.08, b = 0.24 } MIN_VALUE = 1 MAX_VALUE = 10 IMAGE_URL = { @@ -9,120 +9,122 @@ IMAGE_URL = { function onload() self.createButton({ - label="Add", - click_function="addBlessToken", - function_owner=self, - position={-2.3,0.1,-0.5}, - height=150, - width=300, - scale={x=1.75, y=1.75, z=1.75}, - font_size=100, - font_color={ r=1, g=1, b=1 }, - color=BLESS_COLOR + label = "Add", + click_function = "addBlessToken", + function_owner = self, + position = { -2.3, 0.1, -0.5 }, + height = 150, + width = 300, + scale = { x = 1.75, y = 1.75, z = 1.75 }, + font_size = 100, + font_color = { r = 1, g = 1, b = 1 }, + color = BLESS_COLOR }) self.createButton({ - label="Remove", - click_function="removeBlessToken", - function_owner=self, - position={-0.9,0.1,-0.5}, - height=150, - width=450, - scale={x=1.75, y=1.75, z=1.75}, - font_size=100, - font_color={ r=1, g=1, b=1 }, - color=BLESS_COLOR + label = "Remove", + click_function = "removeBlessToken", + function_owner = self, + position = { -0.9, 0.1, -0.5 }, + height = 150, + width = 450, + scale = { x = 1.75, y = 1.75, z = 1.75 }, + font_size = 100, + font_color = { r = 1, g = 1, b = 1 }, + color = BLESS_COLOR }) self.createButton({ - label="Take", - click_function="takeBlessToken", - function_owner=self, - position={0.7,0.1,-0.5}, - height=150, - width=350, - scale={x=1.75, y=1.75, z=1.75}, - font_size=100, - font_color={ r=1, g=1, b=1 }, - color=BLESS_COLOR + label = "Take", + click_function = "takeBlessToken", + function_owner = self, + position = { 0.7, 0.1, -0.5 }, + height = 150, + width = 350, + scale = { x = 1.75, y = 1.75, z = 1.75 }, + font_size = 100, + font_color = { r = 1, g = 1, b = 1 }, + color = BLESS_COLOR }) self.createButton({ - label="Return", - click_function="returnBlessToken", - function_owner=self, - position={2.1,0.1,-0.5}, - height=150, - width=400, - scale={x=1.75, y=1.75, z=1.75}, - font_size=100, - font_color={ r=1, g=1, b=1 }, - color=BLESS_COLOR + label = "Return", + click_function = "returnBlessToken", + function_owner = self, + position = { 2.1, 0.1, -0.5 }, + height = 150, + width = 400, + scale = { x = 1.75, y = 1.75, z = 1.75 }, + font_size = 100, + font_color = { r = 1, g = 1, b = 1 }, + color = BLESS_COLOR }) self.createButton({ - label="Add", - click_function="addCurseToken", - function_owner=self, - position={-2.3,0.1,0.5}, - height=150, - width=300, - scale={x=1.75, y=1.75, z=1.75}, - font_size=100, - font_color={ r=1, g=1, b=1 }, - color=CURSE_COLOR + label = "Add", + click_function = "addCurseToken", + function_owner = self, + position = { -2.3, 0.1, 0.5 }, + height = 150, + width = 300, + scale = { x = 1.75, y = 1.75, z = 1.75 }, + font_size = 100, + font_color = { r = 1, g = 1, b = 1 }, + color = CURSE_COLOR }) self.createButton({ - label="Remove", - click_function="removeCurseToken", - function_owner=self, - position={-0.9,0.1,0.5}, - height=150, - width=450, - scale={x=1.75, y=1.75, z=1.75}, - font_size=100, - font_color={ r=1, g=1, b=1 }, - color=CURSE_COLOR + label = "Remove", + click_function = "removeCurseToken", + function_owner = self, + position = { -0.9, 0.1, 0.5 }, + height = 150, + width = 450, + scale = { x = 1.75, y = 1.75, z = 1.75 }, + font_size = 100, + font_color = { r = 1, g = 1, b = 1 }, + color = CURSE_COLOR }) self.createButton({ - label="Take", - click_function="takeCurseToken", - function_owner=self, - position={0.7,0.1,0.5}, - height=150, - width=350, - scale={x=1.75, y=1.75, z=1.75}, - font_size=100, - font_color={ r=1, g=1, b=1 }, - color=CURSE_COLOR + label = "Take", + click_function = "takeCurseToken", + function_owner = self, + position = { 0.7, 0.1, 0.5 }, + height = 150, + width = 350, + scale = { x = 1.75, y = 1.75, z = 1.75 }, + font_size = 100, + font_color = { r = 1, g = 1, b = 1 }, + color = CURSE_COLOR }) self.createButton({ - label="Return", - click_function="returnCurseToken", - function_owner=self, - position={2.1,0.1,0.5}, - height=150, - width=400, - scale={x=1.75, y=1.75, z=1.75}, - font_size=100, - font_color={ r=1, g=1, b=1 }, - color=CURSE_COLOR + label = "Return", + click_function = "returnCurseToken", + function_owner = self, + position = { 2.1, 0.1, 0.5 }, + height = 150, + width = 400, + scale = { x = 1.75, y = 1.75, z = 1.75 }, + font_size = 100, + font_color = { r = 1, g = 1, b = 1 }, + color = CURSE_COLOR }) self.createButton({ - label="Reset", click_function="doReset", function_owner=self, - position={0,0.3,1.8}, rotation={0,0,0}, height=350, width=800, - font_size=250, color={0,0,0}, font_color={1,1,1} + label = "Reset", click_function = "doReset", function_owner = self, + position = { 0, 0, 1.8 }, rotation = { 0, 0, 0 }, height = 350, width = 800, + font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 } }) - numInPlay = { Bless=0, Curse=0 } - tokensTaken = { Bless={}, Curse={} } + numInPlay = { Bless = 0, Curse = 0 } + tokensTaken = { Bless = {}, Curse = {} } + sealedTokens = {} Wait.time(initializeState, 1) addHotkey("Bless Curse Status", printStatus, false) + addHotkey("Wendy's Menu", addMenuOptions, false) end function initializeState() @@ -131,7 +133,7 @@ function initializeState() local chaosbag = getChaosBag() if chaosbag == nil then return end local tokens = {} - for i,v in ipairs(chaosbag.getObjects()) do + for i, v in ipairs(chaosbag.getObjects()) do if v.name == "Bless" then numInPlay.Bless = numInPlay.Bless + 1 elseif v.name == "Curse" then @@ -140,22 +142,17 @@ function initializeState() end -- find tokens in the play area - local objs = Physics.cast({ - origin = { x=-33, y=0, z=0.5 }, - direction = { x=0, y=1, z=0 }, - type = 3, - size = { x=77, y=5, z=77 }, - orientation = { x=0, y=90, z=0 } - }) - - for i,v in ipairs(objs) do - local obj = v.hit_object - if obj.getName() == "Bless" then - table.insert(tokensTaken.Bless, obj.getGUID()) - numInPlay.Bless = numInPlay.Bless + 1 - elseif obj.getName() == "Curse" then - table.insert(tokensTaken.Curse, obj.getGUID()) - numInPlay.Curse = numInPlay.Curse + 1 + local objs = getObjects() + for i, obj in ipairs(objs) do + local pos = obj.getPosition() + if (pos.x > -110 and pos.x < 44 and pos.z > -77 and pos.z < 77) then + if obj.getName() == "Bless" then + table.insert(tokensTaken.Bless, obj.getGUID()) + numInPlay.Bless = numInPlay.Bless + 1 + elseif obj.getName() == "Curse" then + table.insert(tokensTaken.Curse, obj.getGUID()) + numInPlay.Curse = numInPlay.Curse + 1 + end end end @@ -174,8 +171,8 @@ end function doReset(_obj, _color, alt_click) playerColor = _color - numInPlay = { Bless=0, Curse=0 } - tokensTaken = { Bless={}, Curse={} } + numInPlay = { Bless = 0, Curse = 0 } + tokensTaken = { Bless = {}, Curse = {} } initializeState() end @@ -202,8 +199,8 @@ function spawnToken() local url = IMAGE_URL[mode] local obj = spawnObject({ type = 'Custom_Tile', - position = {pos.x, pos.y + 3, pos.z}, - rotation = {x = 0, y = 260, z = 0}, + position = { pos.x, pos.y + 3, pos.z }, + rotation = { x = 0, y = 260, z = 0 }, callback_function = spawn_callback }) obj.setCustomObject({ @@ -211,7 +208,7 @@ function spawnToken() image = url, thickness = 0.10, }) - obj.scale {0.81, 1, 0.81} + obj.scale { 0.81, 1, 0.81 } return obj end @@ -242,8 +239,14 @@ function takeToken(type, _color, remove) playerColor = _color local chaosbag = getChaosBag() if chaosbag == nil then return end + if not remove and not SEAL_CARD_MESSAGE then + broadcastToColor("Are you trying to seal a token on a card? Return " .. + "this one, then try right-clicking on the card for seal options.", + _color) + SEAL_CARD_MESSAGE = true + end local tokens = {} - for i,v in ipairs(chaosbag.getObjects()) do + for i, v in ipairs(chaosbag.getObjects()) do if v.name == type then table.insert(tokens, v.guid) end @@ -260,9 +263,13 @@ function takeToken(type, _color, remove) end local guid = table.remove(tokens) mode = type + local position = Vector({ pos.x - 2, pos.y, pos.z + 2.5 }) + if type == "Curse" then + position = position + Vector({ 0, 0, -5 }) + end chaosbag.takeObject({ guid = guid, - position = {pos.x-2, pos.y, pos.z}, + position = position, smooth = false, callback_function = callback }) @@ -314,7 +321,7 @@ end function getChaosBag() local items = getObjectFromGUID("83ef06").getObjects() local chaosbag = nil - for i,v in ipairs(items) do + for i, v in ipairs(items) do if v.getDescription() == "Chaos Bag" then chaosbag = getObjectFromGUID(v.getGUID()) break @@ -334,3 +341,104 @@ function getTokenCount() return "(" .. (numInPlay[mode] - #tokensTaken[mode]) .. "/" .. #tokensTaken[mode] .. ")" end + +function addMenuOptions(playerColor, hoveredObject, pointerPosition, isKeyUp) + local manager = self + if hoveredObject == nil or hoveredObject.getVar("MENU_ADDED") == true then return end + if hoveredObject.tag ~= "Card" then + broadcastToColor("Right-click seal options can only be added to cards", playerColor) + return + end + hoveredObject.addContextMenuItem("Seal Bless", function(color) + manager.call("sealToken", { + type = "Bless", + playerColor = color, + enemy = hoveredObject + }) + end, true) + hoveredObject.addContextMenuItem("Release Bless", function(color) + manager.call("releaseToken", { + type = "Bless", + playerColor = color, + enemy = hoveredObject + }) + end, true) + hoveredObject.addContextMenuItem("Seal Curse", function(color) + manager.call("sealToken", { + type = "Curse", + playerColor = color, + enemy = hoveredObject + }) + end, true) + hoveredObject.addContextMenuItem("Release Curse", function(color) + manager.call("releaseToken", { + type = "Curse", + playerColor = color, + enemy = hoveredObject + }) + end, true) + broadcastToColor("Right-click seal options added to " .. hoveredObject.getName(), playerColor) + hoveredObject.setVar("MENU_ADDED", true) + sealedTokens[hoveredObject.getGUID()] = {} +end + +function sealToken(params) + playerColor = params.playerColor + local chaosbag = getChaosBag() + if chaosbag == nil then return end + local pos = params.enemy.getPosition() + local manager = self + + for i, token in ipairs(chaosbag.getObjects()) do + if token.name == params.type then + chaosbag.takeObject({ + position = { pos.x, pos.y + 1, pos.z }, + index = i - 1, + smooth = false, + callback_function = function(obj) + Wait.frames(function() + local mSealedTokens = manager.getVar("sealedTokens") + local tokens = mSealedTokens[params.enemy.getGUID()] + table.insert(tokens, obj) + manager.setVar("sealedTokens", mSealedTokens) + local guid = obj.getGUID() + local tokensTaken = manager.getVar("tokensTaken") + table.insert(tokensTaken[params.type], guid) + manager.setVar("tokensTaken", tokensTaken) + manager.setVar("mode", params.type) + printToColor("Sealing " .. params.type .. " token " .. manager.call("getTokenCount"), + params.playerColor) + end + , 1) + end + }) + return + end + end + printToColor(params.type .. " token not found in bag", playerColor) +end + +function releaseToken(params) + playerColor = params.playerColor + local chaosbag = getChaosBag() + if chaosbag == nil then return end + local tokens = sealedTokens[params.enemy.getGUID()] + if tokens == nil or #tokens == 0 then return end + mode = params.type + + for i, token in ipairs(tokens) do + if token ~= nil and token.getName() == params.type then + local guid = token.getGUID() + chaosbag.putObject(token) + for j, v in ipairs(tokensTaken[mode]) do + if v == guid then + table.remove(tokensTaken[mode], j) + table.remove(tokens, i) + printToColor("Releasing " .. mode .. " token" .. getTokenCount(), params.playerColor) + return + end + end + end + end + printToColor(params.type .. " token not sealed on " .. params.enemy.getName(), params.playerColor) +end diff --git a/src/util/ChaosBagStatTracker.ttslua b/src/util/ChaosBagStatTracker.ttslua new file mode 100644 index 00000000..1856aae2 --- /dev/null +++ b/src/util/ChaosBagStatTracker.ttslua @@ -0,0 +1,103 @@ +function onload(saved_data) + light_mode = false + + if saved_data ~= "" then + local loaded_data = JSON.decode(saved_data) + light_mode = loaded_data[1] + end + createAll() +end + +-- functions delegated to Global +function printStats(object, player, isRightClick) + -- local toPosition = self.positionToWorld(DRAWN_CHAOS_TOKEN_OFFSET) + if isRightClick then + Global.call("resetStats") + else + Global.call("printStats") + end +end + +function updateSave() + local data_to_save = {light_mode } + saved_data = JSON.encode(data_to_save) + self.script_state = saved_data +end + +function createAll() + s_color = {0.5, 0.5, 0.5, 95} + + if light_mode then + f_color = {1,1,1,95} + else + f_color = {0,0,0,100} + end + + self.createButton({ + click_function="printStats", + function_owner=self, + position={0,0.05,0}, + height=600, + width=1000, + alignment = 3, + tooltip = "Left Click to print stats. Right Click to reset them.", + scale={x=1.5, y=1.5, z=1.5}, + font_size=600, + font_color=f_color, + color={0,0,0,0} + }) + + if light_mode then + lightButtonText = "[ Set dark ]" + else + lightButtonText = "[ Set light ]" + end + +end + +function removeAll() + self.removeInput(0) + self.removeInput(1) + self.removeButton(0) + self.removeButton(1) + self.removeButton(2) +end + +function reloadAll() + removeAll() + createAll() + updateSave() +end + +function swap_fcolor(_obj, _color, alt_click) + light_mode = not light_mode + reloadAll() +end + +function swap_align(_obj, _color, alt_click) + center_mode = not center_mode + reloadAll() +end + +function editName(_obj, _string, value) + self.setName(value) + setTooltips() +end + +function setTooltips() + self.editInput({ + index = 0, + value = self.getName(), + tooltip = "Left click to show stats. Right click to reset them." + }) +end + +function keepSample(_obj, _string, value) + reloadAll() +end + +function onDestroy() + if timerID and type(timerID) == 'object' then + Timer.destroy(timerID) + end +end diff --git a/src/util/ClueCounterSwapper.ttslua b/src/util/ClueCounterSwapper.ttslua index f932fb96..c513e03a 100644 --- a/src/util/ClueCounterSwapper.ttslua +++ b/src/util/ClueCounterSwapper.ttslua @@ -1,5 +1,5 @@ function updateSave() - local data_to_save = {["ml"]=memoryList} + local data_to_save = { ["ml"] = memoryList } saved_data = JSON.encode(data_to_save) self.script_state = saved_data end @@ -21,16 +21,20 @@ function onload(saved_data) 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} + 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 @@ -50,7 +54,7 @@ function createButtonsOnAllObjects() 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 + globalScaleFactor = 1.25 * 1 / self.getScale().x --Super sweet math to set button positions local selfPos = self.getPosition() local objPos = obj.getPosition() @@ -67,9 +71,9 @@ function createButtonsOnAllObjects() 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}, + 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 @@ -79,43 +83,40 @@ 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} + 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} + 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} + 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} + local color = { 0, 1, 0, 0.6 } if memoryList[obj.getGUID()] == nil then - self.editButton({index=index, color=color}) + 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() + 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}) + obj.highlightOn({ 0, 1, 0 }) else - color = {0.75,0.25,0.25,0.6} - self.editButton({index=index, color=color}) + color = { 0.75, 0.25, 0.25, 0.6 } + self.editButton({ index = index, color = color }) memoryList[obj.getGUID()] = nil obj.highlightOff() end @@ -131,13 +132,13 @@ function buttonClick_cancel() createMemoryActionButtons() end removeAllHighlights() - broadcastToAll("Selection Canceled", {1,1,1}) + 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}) + broadcastToAll("You cannot submit without any selections.", { 0.75, 0.25, 0.25 }) else self.clearButtons() createMemoryActionButtons() @@ -147,7 +148,7 @@ function buttonClick_submit() local obj = getObjectFromGUID(guid) if obj ~= nil then obj.highlightOff() end end - broadcastToAll(count.." Objects Saved", {1,1,1}) + broadcastToAll(count .. " Objects Saved", { 1, 1, 1 }) updateSave() end end @@ -158,31 +159,55 @@ function buttonClick_reset() self.clearButtons() createSetupButton() removeAllHighlights() - broadcastToAll("Tool Reset", {1,1,1}) + 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} + 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, + 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} }) --- 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 @@ -200,7 +225,7 @@ function buttonClick_place() for _, bagObj in ipairs(bagObjList) do if bagObj.guid == guid then local item = self.takeObject({ - guid=guid, position=entry.pos, rotation=entry.rot, + guid = guid, position = entry.pos, rotation = entry.rot, }) item.setLock(entry.lock) break @@ -208,7 +233,7 @@ function buttonClick_place() end end end - broadcastToAll("Objects Placed", {1,1,1}) + broadcastToAll("Objects Placed", { 1, 1, 1 }) end --Recalls objects to bag from table @@ -217,31 +242,27 @@ function buttonClick_recall() local obj = getObjectFromGUID(guid) if obj ~= nil then self.putObject(obj) end end - broadcastToAll("Objects Recalled", {1,1,1}) + 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) + 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 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=objPos.x+x, y=objPos.y+desiredPos.y, z=objPos.z+z} - return {x=x, y=desiredPos.y, z=z} + 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 @@ -268,6 +289,6 @@ 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 + local mult = 10 ^ (dec or 0) + return math.floor(num * mult + 0.5) / mult end diff --git a/src/util/ConnectionDrawingTool.ttslua b/src/util/ConnectionDrawingTool.ttslua new file mode 100644 index 00000000..7d3e8009 --- /dev/null +++ b/src/util/ConnectionDrawingTool.ttslua @@ -0,0 +1,98 @@ +-- Drawing Tool +-- created by: Chr1Z +-- original by: Whimsical +-- description: draws lines between selected objects +information = { + version = "1.1", + last_updated = "10.10.2022" +} + +-- save "lines" to be able to remove them after loading +function onSave() return JSON.encode(lines) end + +-- load data and add context menu +function onload(saved_data) + lines = JSON.decode(saved_data) or {} + + self.addContextMenuItem("More Information", function() + printToAll("------------------------------", "White") + printToAll("Drawing Tool v" .. information["version"] .. " by Chr1Z", "Orange") + printToAll("last updated: " .. information["last_updated"], "White") + printToAll("original concept by Whimsical", "White") + end) +end + +-- create timer when numpad 0 is pressed +function onScriptingButtonDown(index, player_color) + if index ~= 10 then return end + TimerID = Wait.time(function() draw_from(Player[player_color]) end, 1) +end + +-- called for long press of numpad 0, draws lines from hovered object to selected objects +function draw_from(player) + local source = player.getHoverObject() + if not source then return end + + for _, item in ipairs(player.getSelectedObjects()) do + if item.getGUID() ~= source.getGUID() then + if item.getGUID() > source.getGUID() then + draw_with_pair(item, source) + else + draw_with_pair(source, item) + end + end + end + + process_lines() +end + +-- general drawing of all lines between selected objects +function onScriptingButtonUp(index, player_color) + if index ~= 10 then return end + -- returns true only if there is a timer to cancel. If this is false then we've waited longer than a second. + if not Wait.stop(TimerID) then return end + + local items = Player[player_color].getSelectedObjects() + if #items < 2 then + broadcastToColor("You must have at least two items selected (currently: " .. #items .. ").", player_color, "Red") + return + end + + table.sort(items, function(a, b) return a.getGUID() > b.getGUID() end) + + for f = 1, #items - 1 do + for s = f + 1, #items do + draw_with_pair(items[f], items[s]) + end + end + + process_lines() +end + +-- adds two objects to table of vector lines +function draw_with_pair(first, second) + local guid_first = first.getGUID() + local guid_second = second.getGUID() + + if Global.getVectorLines() == nil then lines = {} end + if not lines[guid_first] then lines[guid_first] = {} end + + if lines[guid_first][guid_second] then + lines[guid_first][guid_second] = nil + else + lines[guid_first][guid_second] = { points = { first.getPosition(), second.getPosition() }, color = "White" } + end +end + +-- updates the global vector lines based on "lines" +function process_lines() + local drawing = {} + + for _, first in pairs(lines) do + for _, data in pairs(first) do + table.insert(drawing, data) + end + end + + Global.setVectorLines(drawing) +end diff --git a/src/util/DeckCutter.ttslua b/src/util/DeckCutter.ttslua new file mode 100644 index 00000000..20efcd77 --- /dev/null +++ b/src/util/DeckCutter.ttslua @@ -0,0 +1,32 @@ +--- +--- Generated by EmmyLua(https://github.com/EmmyLua) +--- Created by Whimsical. +--- DateTime: 2020-09-26 5:50 p.m. +--- + +---@param index number +---@param player_color string +function onScriptingButtonDown(index, player_color) + if not (index>=1 and index<=3) then return end + + local count = index * 3 + + ---@type Player + local player = Player[player_color] + local object = player:getHoverObject() + + + if (not object) then return end + if (object.tag~="Deck") then return end + if (count >= object:getQuantity()) then return end + + for _ =1, count do + local target_position = object:positionToWorld(Vector(0, 0, -3.5)) + + object:takeObject { + index = 0, + position = target_position, + smooth = false + } + end +end diff --git a/src/util/HandSizeCounter.ttslua b/src/util/HandSizeCounter.ttslua deleted file mode 100644 index 7f8a5f40..00000000 --- a/src/util/HandSizeCounter.ttslua +++ /dev/null @@ -1,71 +0,0 @@ -function onload(save_state) - val = 0 - playerColor = "Orange" - if save_state ~= nil then - local obj = JSON.decode(save_state) - if obj ~= nil and obj.playerColor ~= nil then - playerColor = obj.playerColor - end - end - des = false - loopId = Wait.time(|| updateValue(), 1, -1) - self.addContextMenuItem("Bind to my color", bindColor) -end - -function bindColor(player_color) - playerColor = player_color - self.setName(player_color .. " Hand Size Counter") -end - -function onSave() - return JSON.encode({ playerColor = playerColor }) -end - -function onHover(player_color) - if not (player_color == playerColor) then return end - Wait.stop(loopId) - des = not des - updateValue() - des = not des - loopId = Wait.time(|| updateValue(), 1, -1) -end - -function updateDES(player, value, id) - if (value == "True") then des = true - else des = false - end - updateValue() -end - -function updateValue() - local hand = Player[playerColor].getHandObjects() - local size = 0 - - if (des) then - self.UI.setAttribute("handSize", "color", "#00FF00") - -- count by name for Dream Enhancing Serum - local cardHash = {} - for key, obj in pairs(hand) do - if obj != nil and obj.tag == "Card" then - local name = obj.getName() - local title, xp = string.match(name, '(.+)(%s%(%d+%))') - if title ~= nil then name = title end - cardHash[name] = obj - end - end - for key, obj in pairs(cardHash) do - size = size + 1 - end - else - self.UI.setAttribute("handSize", "color", "#FFFFFF") - -- otherwise count individually - for key, obj in pairs(hand) do - if obj != nil and obj.tag == "Card" then - size = size + 1 - end - end - end - - val = size - self.UI.setValue("handSize", val) -end diff --git a/src/util/HandSizeCounter.xml b/src/util/HandSizeCounter.xml deleted file mode 100644 index 4c812548..00000000 --- a/src/util/HandSizeCounter.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - ? - - - DES - - diff --git a/src/util/StatTracker.ttslua b/src/util/StatTracker.ttslua deleted file mode 100644 index e69de29b..00000000