Merge pull request #1 from argonui/initial231
Update source repository to SCED v2.3.1
This commit is contained in:
commit
32a86bd9b4
@ -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<string, CommandTableEntry>
|
||||
local commands = {}
|
||||
|
||||
---@type table<string, boolean>
|
||||
local found_commands = {}
|
||||
|
||||
---@type table<string, any>
|
||||
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<any, any>
|
||||
---@param updates table<any, any>
|
||||
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<string, any>
|
||||
---@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<string, any>
|
||||
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<string, any>
|
||||
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<string, any>
|
||||
---@param handler_constants table<string, any>
|
||||
---@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<string, any>
|
||||
---@param handler_constants table<string, any>
|
||||
---@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
|
@ -1,18 +1,38 @@
|
||||
---
|
||||
--- 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,
|
||||
@ -46,8 +66,6 @@ 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))
|
||||
@ -93,17 +111,14 @@ function onLoad(script_state)
|
||||
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
|
||||
@ -150,62 +165,67 @@ 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
|
||||
-- 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
|
||||
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
|
||||
elseif parallelFront then
|
||||
|
||||
local alternateNum = tonumber(deckMeta.alternate_front)
|
||||
if alternateNum >= 01501 and alternateNum <= 01506 then
|
||||
investigatorId = investigatorId .. "-r"
|
||||
else
|
||||
investigatorId = investigatorId .. "-pf"
|
||||
elseif (parallelBack) then
|
||||
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
|
||||
@ -215,6 +235,8 @@ function extractBondedCards(slots, configuration)
|
||||
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,25 +246,22 @@ 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
|
||||
-- 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
|
||||
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] = nil
|
||||
@ -252,32 +271,98 @@ 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 = {}
|
||||
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
|
||||
@ -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 = {}
|
||||
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,29 +399,44 @@ 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},
|
||||
cardId
|
||||
},
|
||||
function(result)
|
||||
local adbCardInfo = JSON.decode(fixUtf16String(result.text))
|
||||
local cardName = adbCardInfo.real_name
|
||||
@ -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 = {}
|
||||
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
|
||||
@ -396,7 +557,7 @@ end
|
||||
-- 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 = {}
|
||||
deck.Name = "Deck"
|
||||
@ -406,24 +567,23 @@ function buildDeckDataTemplate()
|
||||
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
|
||||
-- <id>-<altVersionTag>-m, and update the entries in cardList with the new card
|
||||
-- data.
|
||||
--
|
||||
-- Param cardList: Deck list being created
|
||||
-- Param altVersionTag: The tag for the different version, currently the only
|
||||
-- alt versions are "promo", but will soon inclide "revised"
|
||||
-- Param configuration: ArkhamDB configuration defniition, used for the card bag
|
||||
---@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
|
||||
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
|
||||
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
|
||||
-- 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,7 +670,7 @@ function handleAncestralKnowledge(cardList)
|
||||
table.insert(skillList, i)
|
||||
end
|
||||
end
|
||||
if (hasAncestralKnowledge) then
|
||||
if hasAncestralKnowledge then
|
||||
for i = 1, 5 do
|
||||
-- Move 5 random skills to SetAside3
|
||||
local skillListIndex = math.random(#skillList)
|
||||
@ -528,8 +680,90 @@ 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
|
||||
@ -551,8 +785,7 @@ function testLoadLotsOfDecks()
|
||||
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,9 +800,8 @@ 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
|
||||
@ -580,7 +812,8 @@ function buildDeck(playerColor, deckId)
|
||||
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, "<!DOCTYPE html>") then
|
||||
@ -606,7 +839,7 @@ Request = {
|
||||
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
|
||||
@ -629,8 +862,8 @@ function Request:new(uri, configure)
|
||||
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
|
||||
@ -651,7 +884,7 @@ function Request.deferred(uri, on_success, on_error, ...)
|
||||
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
|
||||
|
@ -1,14 +1,15 @@
|
||||
local INPUT_FIELD_HEIGHT = 340
|
||||
local INPUT_FIELD_WIDTH = 1500
|
||||
|
||||
local FIELD_COLOR = { 0.9, 0.7, 0.5 }
|
||||
|
||||
local PRIVATE_TOGGLE_LABELS = {}
|
||||
PRIVATE_TOGGLE_LABELS[true] = "Private"
|
||||
PRIVATE_TOGGLE_LABELS[false] = "Published"
|
||||
|
||||
local UPGRADED_TOGGLE_LABELS = {}
|
||||
UPGRADED_TOGGLE_LABELS[true] = "Upgraded"
|
||||
UPGRADED_TOGGLE_LABELS[false] = "Specific"
|
||||
|
||||
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.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.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.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
|
||||
|
||||
@ -165,8 +132,7 @@ function makeDeckIdFields()
|
||||
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"
|
||||
@ -179,57 +145,29 @@ function makeBuildButton()
|
||||
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()
|
||||
|
@ -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<string, ArkhamImportTaboo>
|
||||
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 <string, boolean>
|
||||
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<string, boolean>
|
||||
---@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<string, number>
|
||||
---@return Request[]
|
||||
local function load_cards(configuration, slots)
|
||||
---@type Request[]
|
||||
local requests = {}
|
||||
|
||||
---@type <string, boolean>
|
||||
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<string, string>
|
||||
---@param card ArkhamImportCard
|
||||
---@param taboo ArkhamImportTaboo
|
||||
---@param meta table<string, any>
|
||||
---@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, ": <table>"}, 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, "<!DOCTYPE html>") 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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,
|
||||
|
@ -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
|
@ -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 <LOCATION_NAME>_<GUID> and if
|
||||
we find nothing we look for <LOCATION_NAME>
|
||||
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
|
@ -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' } },
|
||||
|
132
src/core/DoomInPlayCounter.ttslua
Normal file
132
src/core/DoomInPlayCounter.ttslua
Normal file
@ -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
|
@ -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
|
||||
|
@ -1,199 +1,116 @@
|
||||
-- Playmat Image Swapper
|
||||
-- updated by: Chr1Z
|
||||
-- original by: -
|
||||
-- description: changes the big playmats image
|
||||
information = {
|
||||
version = "1.1",
|
||||
last_updated = "10.10.2022"
|
||||
}
|
||||
|
||||
defaultURL = "http://cloud-3.steamusercontent.com/ugc/998015670465071049/FFAE162920D67CF38045EFBD3B85AD0F916147B2/"
|
||||
|
||||
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")
|
||||
|
||||
-- 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
|
||||
|
||||
|
||||
|
||||
--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
|
||||
-- click function for main button
|
||||
function click_toggleControl()
|
||||
self.clearButtons()
|
||||
self.clearInputs()
|
||||
|
||||
controlActive = not controlActive
|
||||
createOpenCloseButton()
|
||||
|
||||
end
|
||||
end
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
--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})
|
||||
-- click function for apply button
|
||||
function click_applySurface(_, _, isRightClick)
|
||||
if isRightClick then
|
||||
updateSurface(defaultURL)
|
||||
else
|
||||
updateSurface(self.getInputs()[1].value)
|
||||
end
|
||||
end
|
||||
|
||||
--Updates surface from the values in the input field
|
||||
function updateSurface()
|
||||
-- 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()
|
||||
customInfo.image = self.getInputs()[1].value
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
--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
|
||||
|
||||
|
||||
|
||||
-- creates the main button
|
||||
function createOpenCloseButton()
|
||||
local tooltip = "Open Playmat Panel"
|
||||
if controlActive then
|
||||
tooltip = "Close Playmat Panel"
|
||||
BUTTON_PARAMETERS.tooltip = "Close Playmat Panel"
|
||||
else
|
||||
BUTTON_PARAMETERS.tooltip = "Open 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
|
||||
})
|
||||
self.createButton(BUTTON_PARAMETERS)
|
||||
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"
|
||||
}
|
||||
|
||||
ref_playerColor = {
|
||||
"White", "Brown", "Red", "Orange", "Yellow",
|
||||
"Green", "Teal", "Blue", "Purple", "Pink", "Black"
|
||||
}
|
||||
|
||||
--Dummy function, absorbs unwanted triggers
|
||||
function none() end
|
||||
|
@ -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,6 +119,9 @@ function buildSupplementalIndexes()
|
||||
for cardId, card in pairs(cardIdIndex) do
|
||||
local cardData = card.data
|
||||
local cardMetadata = card.metadata
|
||||
-- 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
|
||||
@ -130,6 +138,8 @@ 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")
|
||||
@ -162,6 +172,7 @@ function buildSupplementalIndexes()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, indexTable in pairs(classAndLevelIndex) do
|
||||
table.sort(indexTable, cardComparator)
|
||||
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
|
||||
|
||||
return weaknessId
|
||||
-- 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
|
||||
|
||||
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
|
||||
|
136
src/playercards/CardSearch.ttslua
Normal file
136
src/playercards/CardSearch.ttslua
Normal file
@ -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
|
@ -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,
|
||||
|
@ -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,31 +118,28 @@ 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
|
||||
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
|
||||
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)
|
||||
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
|
||||
|
||||
@ -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 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
|
||||
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 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'])
|
||||
spawnTokenGroup(object, data['tokenType'], data['tokenCount'])
|
||||
log("Spawning tokens for "..object.getName()..'['..object.getDescription()..']: '..tokenCount.."x "..token)
|
||||
spawnTokenGroup(object, token, tokenCount)
|
||||
end
|
||||
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)
|
@ -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
|
@ -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
|
@ -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
|
132
src/playermat/SkillToken.ttslua
Normal file
132
src/playermat/SkillToken.ttslua
Normal file
@ -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
|
164
src/playermat/Zones.ttslua
Normal file
164
src/playermat/Zones.ttslua
Normal file
@ -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
|
@ -114,15 +114,17 @@ function onload()
|
||||
|
||||
self.createButton({
|
||||
label = "Reset", click_function = "doReset", function_owner = self,
|
||||
position={0,0.3,1.8}, rotation={0,0,0}, height=350, width=800,
|
||||
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 = {} }
|
||||
sealedTokens = {}
|
||||
Wait.time(initializeState, 1)
|
||||
|
||||
addHotkey("Bless Curse Status", printStatus, false)
|
||||
addHotkey("Wendy's Menu", addMenuOptions, false)
|
||||
end
|
||||
|
||||
function initializeState()
|
||||
@ -140,16 +142,10 @@ 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
|
||||
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
|
||||
@ -158,6 +154,7 @@ function initializeState()
|
||||
numInPlay.Curse = numInPlay.Curse + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
mode = "Bless"
|
||||
print("Bless Tokens " .. getTokenCount())
|
||||
@ -242,6 +239,12 @@ 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
|
||||
if v.name == type then
|
||||
@ -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
|
||||
})
|
||||
@ -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
|
||||
|
103
src/util/ChaosBagStatTracker.ttslua
Normal file
103
src/util/ChaosBagStatTracker.ttslua
Normal file
@ -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
|
@ -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
|
||||
|
||||
@ -95,10 +99,7 @@ function createSetupActionButtons()
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
--During Setup
|
||||
|
||||
|
||||
--Checks or unchecks buttons
|
||||
function buttonClick_selection(index, obj)
|
||||
local color = { 0, 1, 0, 0.6 }
|
||||
@ -162,10 +163,7 @@ function buttonClick_reset()
|
||||
updateSave()
|
||||
end
|
||||
|
||||
|
||||
--After Setup
|
||||
|
||||
|
||||
--Creates recall and place buttons
|
||||
function createMemoryActionButtons()
|
||||
self.createButton({
|
||||
@ -178,11 +176,38 @@ function createMemoryActionButtons()
|
||||
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="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 = "Add Draw 1 Buttons", click_function = "addDraw1Buttons", function_owner = self,
|
||||
position = { 0, 1, -2.5 }, rotation = { 0, 0, 0 }, height = 500, width = 2600,
|
||||
font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
|
||||
})
|
||||
--[[
|
||||
self.createButton({
|
||||
label="Setup", click_function="buttonClick_setup", function_owner=self,
|
||||
position={-6,1,0}, rotation={0,90,0}, height=500, width=1200,
|
||||
font_size=350, color={0,0,0}, font_color={1,1,1}
|
||||
})
|
||||
--]]
|
||||
end
|
||||
|
||||
function addDraw1Buttons()
|
||||
if ADD_BUTTONS_DISABLED then return end
|
||||
|
||||
local mats = { "8b081b", "bd0ff4", "383d8b", "0840d5" }
|
||||
for i, guid in ipairs(mats) do
|
||||
local mat = getObjectFromGUID(guid)
|
||||
mat.createButton({
|
||||
label = "Draw 1",
|
||||
click_function = "doDrawOne",
|
||||
function_owner = mat,
|
||||
position = { 1.84, 0.1, -0.36 },
|
||||
scale = { 0.12, 0.12, 0.12 },
|
||||
width = 800,
|
||||
height = 280,
|
||||
font_size = 180
|
||||
})
|
||||
end
|
||||
ADD_BUTTONS_DISABLED = true
|
||||
end
|
||||
|
||||
--Sends objects from bag/table to their saved position/rotation
|
||||
@ -220,10 +245,7 @@ function buttonClick_recall()
|
||||
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 = {}
|
||||
@ -240,7 +262,6 @@ function rotateLocalCoordinates(desiredPos, obj)
|
||||
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 }
|
||||
end
|
||||
|
||||
|
98
src/util/ConnectionDrawingTool.ttslua
Normal file
98
src/util/ConnectionDrawingTool.ttslua
Normal file
@ -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
|
32
src/util/DeckCutter.ttslua
Normal file
32
src/util/DeckCutter.ttslua
Normal file
@ -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
|
@ -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
|
@ -1,13 +0,0 @@
|
||||
<Defaults>
|
||||
<Text color="#FFFFFF" fontSize="72"/>
|
||||
<Toggle textColor="#FFFFFF"/>
|
||||
</Defaults>
|
||||
|
||||
<VerticalLayout width="150" height="200" position="0 0 -15" rotation="180 180 0">
|
||||
<Row>
|
||||
<Text id="handSize" width="100" height="150" alignment="LowerCenter">?</Text>
|
||||
</Row>
|
||||
<Row>
|
||||
<Toggle id="des" width="100" height="50" onValueChanged="updateDES">DES</Toggle>
|
||||
</Row>
|
||||
</VerticalLayout>
|
Loading…
Reference in New Issue
Block a user