Merge pull request #1 from argonui/initial231

Update source repository to SCED v2.3.1
This commit is contained in:
Buhallin 2022-10-25 04:21:47 -07:00 committed by GitHub
commit 32a86bd9b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 2524 additions and 4442 deletions

View File

@ -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

View File

@ -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,
@ -27,11 +47,11 @@ local printPriority = Priority.INFO
---@param priority number
---@return string
function Priority.getLabel(priority)
if priority==0 then return "ERROR"
elseif priority==1 then return "WARNING"
elseif priority==2 then return "INFO"
elseif priority==3 then return "DEBUG"
else error(table.concat({"Priority", priority, "not found"}, " ")) return ""
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
@ -46,11 +66,9 @@ 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))
return str:gsub("\\u(%w%w%w%w)", function(match)
return string.char(tonumber(match, 16))
end)
end
@ -74,7 +92,7 @@ function onLoad(script_state)
math.randomseed(os.time())
local configuration = getConfiguration()
Request.start({configuration.api_uri, configuration.taboo}, function (status)
Request.start({ configuration.api_uri, configuration.taboo }, function(status)
local json = JSON.decode(fixUtf16String(status.text))
for _, taboo in pairs(json) do
---@type <string, boolean>
@ -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
@ -112,14 +127,14 @@ local function onDeckResult(deck, playerColor, configuration)
return
end
debugPrint(table.concat({ "Found decklist: ", deck.name}), Priority.INFO, playerColor)
debugPrint(table.concat({ "Found decklist: ", deck.name }), Priority.INFO, playerColor)
debugPrint(table.concat({"-", deck.name, "-"}), Priority.DEBUG)
for k,v in pairs(deck) do
if type(v)=="table" then
debugPrint(table.concat {k, ": <table>"}, Priority.DEBUG)
debugPrint(table.concat({ "-", deck.name, "-" }), Priority.DEBUG)
for k, v in pairs(deck) do
if type(v) == "table" then
debugPrint(table.concat { k, ": <table>" }, Priority.DEBUG)
else
debugPrint(table.concat {k, ": ", tostring(v)}, Priority.DEBUG)
debugPrint(table.concat { k, ": ", tostring(v) }, Priority.DEBUG)
end
end
debugPrint("", Priority.DEBUG)
@ -150,71 +165,78 @@ local function onDeckResult(deck, playerColor, configuration)
return
end
loadCards(slots, playerColor, commandManager, configuration, results.configuration)
-- get upgrades for customizable cards
local meta = deck.meta
local customizations = {}
if meta then customizations = JSON.decode(deck.meta) end
loadCards(slots, deck.investigator_code, playerColor, commandManager,
configuration, results.configuration, customizations)
end
-- Checks to see if the slot list includes the random weakness ID. If it does,
-- removes it from the deck and replaces it with the ID of a random basic
-- weakness provided by the all cards bag
-- Param slots: The slot list for cards in this deck. Table key is the cardId,
-- value is the number of those cards which will be spawned
-- Param playerColor: Color name of the player this deck is being loaded for.
-- Used for broadcast if a weakness is added.
-- Param configuration: The API configuration object
-- removes it from the deck and replaces it with the ID of a random basic weakness provided by the all cards bag
---@param slots: The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
---@param playerColor: Color name of the player this deck is being loaded for. Used for broadcast if a weakness is added.
---@param configuration: The API configuration object
function maybeDrawRandomWeakness(slots, playerColor, configuration)
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
local hasRandomWeakness = false
for cardId, cardCount in pairs(slots) do
if (cardId == RANDOM_WEAKNESS_ID) then
if cardId == RANDOM_WEAKNESS_ID then
hasRandomWeakness = true
break
end
end
if (hasRandomWeakness) then
if hasRandomWeakness then
local weaknessId = allCardsBag.call("getRandomWeaknessId")
slots[weaknessId] = 1
slots[RANDOM_WEAKNESS_ID] = nil
debugPrint("Random basic weakness added to deck", Priority.INFO, playerColor)
end
end
-- If the UI indicates that investigator cards should be loaded, add both the
-- investigator (XXXXX) and minicard (XXXXX-m) slots with one copy each
-- Param deck: The processed ArkhamDB deck response
-- Param slots: The slot list for cards in this deck. Table key is the cardId,
-- value is the number of those cards which will be spawned
-- If investigator cards should be loaded, add both the investigator (XXXXX) and minicard (XXXXX-m) slots with one copy each
---@param deck: The processed ArkhamDB deck response
---@param slots: The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
function maybeAddInvestigatorCards(deck, slots)
if (getUiState().investigators) then
if getUiState().investigators then
local investigatorId = deck.investigator_code
slots[investigatorId.."-m"] = 1
local parallelFront = deck.meta ~= nil and deck.meta.alternate_front ~= nil and deck.meta.alternate_front ~= ""
local parallelBack = deck.meta ~= nil and deck.meta.alternate_back ~= nil and deck.meta.alternate_back ~= ""
if (parallelFront and parallelBack) then
investigatorId = investigatorId.."-p"
elseif (parallelFront) then
investigatorId = investigatorId.."-pf"
elseif (parallelBack) then
investigatorId = investigatorId.."-pb"
slots[investigatorId .. "-m"] = 1
local deckMeta = JSON.decode(deck.meta)
local parallelFront = deckMeta ~= nil and deckMeta.alternate_front ~= nil and deckMeta.alternate_front ~= ""
local parallelBack = deckMeta ~= nil and deckMeta.alternate_back ~= nil and deckMeta.alternate_back ~= ""
if parallelFront and parallelBack then
investigatorId = investigatorId .. "-p"
elseif parallelFront then
local alternateNum = tonumber(deckMeta.alternate_front)
if alternateNum >= 01501 and alternateNum <= 01506 then
investigatorId = investigatorId .. "-r"
else
investigatorId = investigatorId .. "-pf"
end
elseif parallelBack then
investigatorId = investigatorId .. "-pb"
end
slots[investigatorId] = 1
end
end
-- Process the slot list and looks for any cards which are bonded to those in
-- the deck. Adds those cards to the slot list.
-- Param slots: The slot list for cards in this deck. Table key is the cardId,
-- value is the number of those cards which will be spawned
-- Param configuration: The API configuration object
-- Process the slot list and looks for any cards which are bonded to those in the deck. Adds those cards to the slot list.
---@param slots: The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
---@param configuration: The API configuration object
function extractBondedCards(slots, configuration)
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
-- Create a list of bonded cards first so we don't modify slots while iterating
local bondedCards = { }
local bondedCards = {}
for cardId, cardCount in pairs(slots) do
local card = allCardsBag.call("getCardById", { id = cardId })
if (card ~= nil and card.metadata.bonded ~= nil) then
for _, bond in ipairs(card.metadata.bonded) do
bondedCards[bond.id] = bond.count
-- We need to know which cards are bonded to determine their position, remember them
bondedList[bond.id] = true
end
end
end
@ -224,27 +246,24 @@ function extractBondedCards(slots, configuration)
end
end
-- Check the deck for any cards on its taboo list. If they're found, replace
-- the entry in the slot with the Taboo id (i.e. "XXXX" becomes "XXXX-t")
-- Param tabooId: The deck's taboo ID, taken from the deck response taboo_id
-- field. May be nil, indicating that no taboo list should be used
-- Param slots: The slot list for cards in this deck. Table key is the cardId,
-- value is the number of those cards which will be spawned
-- Check the deck for cards on its taboo list. If they're found, replace the entry in the slot with the Taboo id (i.e. "XXXX" becomes "XXXX-t")
---@param tabooId: The deck's taboo ID, taken from the deck response taboo_id field. May be nil, indicating that no taboo list should be used
---@param slots: The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
function checkTaboos(tabooId, slots, playerColor, configuration)
if (tabooId) then
if tabooId then
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
for cardId, _ in pairs(tabooList[tabooId].cards) do
if (slots[cardId] ~= nil) then
if slots[cardId] ~= nil then
-- Make sure there's a taboo version of the card before we replace it
-- SCED only maintains the most recent taboo cards. If a deck is using
-- an older taboo list it's possible the card isn't a taboo any more
local tabooCard = allCardsBag.call("getCardById", { id = cardId.."-t" })
if (tabooCard == nil) then
local tabooCard = allCardsBag.call("getCardById", { id = cardId .. "-t" })
if tabooCard == nil then
local basicCard = allCardsBag.call("getCardById", { id = cardId })
debugPrint("Taboo version for "..basicCard.data.Nickname..
" is not available. Using standard version", Priority.WARNING, playerColor)
debugPrint("Taboo version for " .. basicCard.data.Nickname .. " is not available. Using standard version",
Priority.WARNING, playerColor)
else
slots[cardId.."-t"] = slots[cardId]
slots[cardId .. "-t"] = slots[cardId]
slots[cardId] = nil
end
end
@ -252,38 +271,104 @@ function checkTaboos(tabooId, slots, playerColor, configuration)
end
end
-- Process the slot list, which defines the card Ids and counts of cards to
-- load. Spawn those cards at the appropriate zones, and report an error to the
-- user if any could not be loaded.
-- Process the slot list, which defines the card Ids and counts of cards to load. Spawn those cards at the appropriate zones
-- and report an error to the user if any could not be loaded.
-- This method uses an encapsulated coroutine with yields to make the card spawning cleaner.
--
-- This method uses an encapsulated coroutine with yields to make the card
-- spawning cleaner.
--
-- Param slots: Key-Value table of cardId:count. cardId is the ArkhamDB ID of
-- the card to spawn, and count is the number which should be spawned
-- Param playerColor String Color name of the player mat to place this deck
-- on (e.g. "Red")
-- Param commandManager
-- Param configuration: Loader configuration object
-- Param command_config:
function loadCards(slots, playerColor, commandManager, configuration, command_config)
---@param slots: Key-Value table of cardId:count. cardId is the ArkhamDB ID of the card to spawn, and count is the number which should be spawned
---@param investigatorId: String ArkhamDB ID (code) for this deck's investigator.
-- Investigator cards should already be added to the slots list if they
-- should be spawned, but this value is separate to check for special
-- handling for certain investigators
---@param playerColor String Color name of the player mat to place this deck on (e.g. "Red")
---@param configuration: Loader configuration object
---@param customizations: ArkhamDB data for customizations on customizable cards
function loadCards(slots, investigatorId, playerColor, commandManager, configuration, command_config, customizations)
function coinside()
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
local yPos = { }
local cardsToSpawn = { }
local yPos = {}
local cardsToSpawn = {}
for cardId, cardCount in pairs(slots) do
local card = allCardsBag.call("getCardById", { id = cardId })
if (card ~= nil) then
if card ~= nil then
local cardZone = Zones.getDefaultCardZone(card.metadata)
for i = 1, cardCount do
table.insert(cardsToSpawn, { data = card.data, metadata = card.metadata, zone = cardZone })
end
-- upgrade sheets for customizable cards
local upgradesheet = allCardsBag.call("getCardById", { id = cardId .. "-c" })
if upgradesheet ~= nil then
-- update metadata for spawned upgrade sheets
local upgrades = customizations["cus_" .. cardId]
if upgrades ~= nil then
-- initialize tables
-- markedBoxes: contains the amount of markedBoxes (left to right) per row (starting at row 1)
-- inputValues: contains the amount of inputValues per row (starting at row 0)
local markedBoxes = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
local inputValues = {}
local index_xp = {}
-- get the index and xp values (looks like this: X|X,X|X, ..)
for str in string.gmatch(customizations["cus_" .. cardId], "([^,]+)") do
table.insert(index_xp, str)
end
-- split each pair and assign it to the proper position in markedBoxes
if (customizationRowsWithFields[cardId] ~= nil) then
for i = 1, customizationRowsWithFields[cardId].inputCount do
table.insert(inputValues, "")
end
end
local inputCount = 0
for _, entry in ipairs(index_xp) do
local counter = 0
local index = 0
-- if found number is 0, then only get inputvalue
for str in string.gmatch(entry, "([^|]+)") do
counter = counter + 1
if counter == 1 then
index = tonumber(str) + 1
elseif counter == 2 then
markedBoxes[index] = tonumber(str)
elseif counter == 3 and str ~= "" then
if (cardId == "09042") then
inputValues[customizationRowsWithFields[cardId].inputMap[index]] =
convertRavenQuillSelections(str)
else
inputValues[customizationRowsWithFields[cardId].inputMap[index]] = str
end
end
end
end
-- remove first entry in markedBoxes if row 0 has textbox
if customizationRowsWithFields[cardId] ~= nil
and customizationRowsWithFields[cardId].inputCount > 0 then
table.remove(markedBoxes, 1)
end
-- write the loaded values to the save_data of the sheets
upgradesheet.data["LuaScriptState"] = JSON.encode({ markedBoxes, inputValues })
table.insert(cardsToSpawn, { data = upgradesheet.data, metadata = upgradesheet.metadata, zone = "SetAside4" })
end
end
-- spawn additional minicard for 'Summoned Servitor'
if cardId == "09080" then
local servitor = allCardsBag.call("getCardById", { id = "09080-m" })
table.insert(cardsToSpawn, { data = servitor.data, metadata = servitor.metadata, zone = "SetAside6" })
end
slots[cardId] = 0
end
end
-- TODO: Re-enable this later, as a command
--handleAltInvestigatorCard(cardsToSpawn, "promo", configuration)
-- handleAltInvestigatorCard(cardsToSpawn, "promo", configuration)
table.sort(cardsToSpawn, cardComparator)
@ -292,21 +377,21 @@ function loadCards(slots, playerColor, commandManager, configuration, command_co
-- These should probably be commands, once the command handler is updated
handleStartsInPlay(cardsToSpawn)
handleAncestralKnowledge(cardsToSpawn)
handleUnderworldMarket(cardsToSpawn, playerColor)
handleHunchDeck(investigatorId, cardsToSpawn, playerColor)
-- Count the number of cards in each zone so we know if it's a deck or card.
-- TTS's Card vs. Deck distinction requires this since we can't spawn a deck
-- with only one card
-- TTS's Card vs. Deck distinction requires this since we can't spawn a deck with only one card
local zoneCounts = getZoneCounts(cardsToSpawn)
local zoneDecks = { }
local zoneDecks = {}
for zone, count in pairs(zoneCounts) do
if (count > 1) then
if count > 1 then
zoneDecks[zone] = buildDeckDataTemplate()
end
end
-- For each card in a deck zone, add it to that deck. Otherwise, spawn it
-- directly
-- For each card in a deck zone, add it to that deck. Otherwise, spawn it directly
for _, spawnCard in ipairs(cardsToSpawn) do
if (zoneDecks[spawnCard.zone] ~= nil) then
if zoneDecks[spawnCard.zone] ~= nil then
addCardToDeck(zoneDecks[spawnCard.zone], spawnCard.data)
else
local cardPos = Zones.getZonePosition(playerColor, spawnCard.zone)
@ -314,39 +399,54 @@ 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
if (cardName ~= nil) then
if (adbCardInfo.xp ~= nil and adbCardInfo.xp > 0) then
cardName = cardName.." ("..adbCardInfo.xp..")"
cardName = cardName .. " (" .. adbCardInfo.xp .. ")"
end
debugPrint("Card not found: "..cardName..", ArkhamDB ID "..cardId, Priority.ERROR, playerColor)
debugPrint("Card not found: " .. cardName .. ", ArkhamDB ID " .. cardId, Priority.ERROR, playerColor)
else
debugPrint("Card not found in ArkhamDB, ID "..cardId, Priority.ERROR, playerColor)
debugPrint("Card not found in ArkhamDB, ID " .. cardId, Priority.ERROR, playerColor)
end
end)
end
@ -356,9 +456,38 @@ function loadCards(slots, playerColor, commandManager, configuration, command_co
end
return 1
end
startLuaCoroutine(self, "coinside")
end
-- Conver the Raven Quill's selections from card IDs to card names. This could be more elegant
-- but the inputs are very static so we're using some brute force.
-- @param An ArkhamDB string indicating the customization selections for The Raven's Quill. Should
-- be either a single card ID or two separated by a ^ (e.g. XXXXX^YYYYY)
function convertRavenQuillSelections(selectionString)
if (string.len(selectionString) == 5) then
return getCardName(selectionString)
elseif (string.len(selectionString) == 11) then
return getCardName(string.sub(selectionString, 1, 5))..", "..getCardName(string.sub(selectionString, 7))
end
end
-- Returns the simple name of a card given its ID. This will find the card and strip any trailing
-- SCED-specific suffixes such as (Taboo) or (Level)
function getCardName(cardId)
local configuration = getConfiguration()
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
local card = allCardsBag.call("getCardById", { id = cardId })
if (card ~= nil) then
local name = card.data.Nickname
if (string.find(name, " %(")) then
return string.sub(name, 1, string.find(name, " %(") - 1)
else
return name
end
end
end
-- Inserts a card into the given deck. This does three things:
-- 1. Add the card's data to ContainedObjects
-- 2. Add the card's ID (the TTS CardID, not the Arkham ID) to the deck's
@ -367,23 +496,55 @@ end
-- 3. Extract the card's CustomDeck table and add it to the deck. The deck's
-- "CustomDeck" field is a list of all CustomDecks used by cards within the
-- deck, keyed by the DeckID and referencing the custom deck table
-- Param deck: TTS deck data structure to add to
-- Param card: Data for the card to be inserted
---@param deck: TTS deck data structure to add to
---@param card: Data for the card to be inserted
function addCardToDeck(deck, cardData)
for customDeckId, customDeckData in pairs(cardData.CustomDeck) do
if (deck.CustomDeck[customDeckId] == nil) then
-- CustomDeck not added to deck yet, add it
deck.CustomDeck[customDeckId] = customDeckData
elseif (deck.CustomDeck[customDeckId].FaceURL == customDeckData.FaceURL) then
-- CustomDeck for this card matches the current one for the deck, do nothing
else
-- CustomDeck data conflict
local newDeckId = nil
for deckId, customDeck in pairs(deck.CustomDeck) do
if (customDeckData.FaceURL == customDeck.FaceURL) then
newDeckId = deckId
end
end
if (newDeckId == nil) then
-- No non-conflicting custom deck for this card, add a new one
newDeckId = findNextAvailableId(deck.CustomDeck, "1000")
deck.CustomDeck[newDeckId] = customDeckData
end
-- Update the card with the new CustomDeck info
cardData.CardID = newDeckId..string.sub(cardData.CardID, 5)
cardData.CustomDeck[customDeckId] = nil
cardData.CustomDeck[newDeckId] = customDeckData
break
end
end
table.insert(deck.ContainedObjects, cardData)
table.insert(deck.DeckIDs, cardData.CardID)
for customDeckId, customDeckData in pairs(cardData.CustomDeck) do
deck.CustomDeck[customDeckId] = customDeckData
end
function findNextAvailableId(objectTable, startId)
local id = startId
while (objectTable[id] ~= nil) do
id = tostring(tonumber(id) + 1)
end
return id
end
-- Count the number of cards in each zone
-- Param cards: Table of {cardData, cardMetadata, zone}
-- Return: Table of {zoneName=zoneCount}
---@param cards: Table of {cardData, cardMetadata, zone}
---@return: Table of {zoneName=zoneCount}
function getZoneCounts(cards)
local counts = { }
local counts = {}
for _, card in ipairs(cards) do
if (counts[card.zone] == nil) then
if counts[card.zone] == nil then
counts[card.zone] = 1
else
counts[card.zone] = counts[card.zone] + 1
@ -396,34 +557,33 @@ 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 = { }
local deck = {}
deck.Name = "Deck"
-- Card data. DeckIDs and CustomDeck entries will be built from the cards
deck.ContainedObjects = { }
deck.DeckIDs = { }
deck.CustomDeck = { }
deck.ContainedObjects = {}
deck.DeckIDs = {}
deck.CustomDeck = {}
-- Transform is required, Position and Rotation will be overridden by the
-- spawn call so can be omitted here
-- Transform is required, Position and Rotation will be overridden by the spawn call so can be omitted here
deck.Transform = {
scaleX = 1,
scaleY = 1,
scaleZ = 1, }
scaleZ = 1,
}
return deck
end
-- Get the PBN (Permanent/Bonded/Normal) value from the given metadata.
-- Return: 1 for Permanent, 2 for Bonded, or 3 for Normal. The actual values
-- are irrelevant as they provide only grouping and the order between them
-- doesn't matter.
function getPbn(metadata)
if (metadata.permanent) then
-- Get the PBCN (Permanent/Bonded/Customizable/Normal) value from the given metadata.
---@return: 1 for Permanent, 2 for Bonded or 4 for Normal. The actual values are
-- irrelevant as they provide only grouping and the order between them doesn't matter.
function getpbcn(metadata)
if metadata.permanent then
return 1
elseif (metadata.bonded_to ~= nil) then
elseif metadata.bonded_to ~= nil then
return 2
else -- Normal card
return 3
@ -432,26 +592,26 @@ end
-- Comparison function used to sort the cards in a deck. Groups bonded or
-- permanent cards first, then sorts within theose types by name/subname.
-- Normal cards will sort in standard alphabetical order, while permanent/bonded
-- will be in reverse alphabetical order.
-- Normal cards will sort in standard alphabetical order, while
-- permanent/bonded/customizable will be in reverse alphabetical order.
--
-- Since cards spawn in the order provided by this comparator, with the first
-- cards ending up at the bottom of a pile, this ordering will spawn in reverse
-- alphabetical order. This presents the cards in order for non-face-down
-- areas, and presents them in order when Searching the face-down deck.
function cardComparator(card1, card2)
local pbn1 = getPbn(card1.metadata)
local pbn2 = getPbn(card2.metadata)
if (pbn1 ~= pbn2) then
return pbn1 > pbn2
local pbcn1 = getpbcn(card1.metadata)
local pbcn2 = getpbcn(card2.metadata)
if pbcn1 ~= pbcn2 then
return pbcn1 > pbcn2
end
if (pbn1 == 3) then
if (card1.data.Nickname ~= card2.data.Nickname) then
if pbcn1 == 3 then
if card1.data.Nickname ~= card2.data.Nickname then
return card1.data.Nickname < card2.data.Nickname
end
return card1.data.Description < card2.data.Description
else
if (card1.data.Nickname ~= card2.data.Nickname) then
if card1.data.Nickname ~= card2.data.Nickname then
return card1.data.Nickname > card2.data.Nickname
end
return card1.data.Description > card2.data.Description
@ -463,25 +623,24 @@ end
-- <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
local altInvestigator = allCardsBag.call("getCardById", { id = card.metadata.id.."-"..altVersionTag})
if card.metadata.type == "Investigator" then
local altInvestigator = allCardsBag.call("getCardById", { id = card.metadata.id .. "-" .. altVersionTag })
if (altInvestigator ~= nil) then
card.data = altInvestigator.data
card.metadata = altInvestigator.metadata
end
end
if (card.metadata.type == "Minicard") then
if card.metadata.type == "Minicard" then
-- -promo comes before -m in the ID, so needs a little massaging
local investigatorId = string.sub(card.metadata.id, 1, 5)
local altMinicard = allCardsBag.call("getCardById", { id = investigatorId.."-"..altVersionTag.."-m"})
if (altMinicard ~= nil) then
local altMinicard = allCardsBag.call("getCardById", { id = investigatorId .. "-" .. altVersionTag .. "-m" })
if altMinicard ~= nil then
card.data = altMinicard.data
card.metadata = altMinicard.metadata
end
@ -492,24 +651,17 @@ end
-- Place cards which start in play (Duke, Sophie) in the play area
function handleStartsInPlay(cardList)
for _, card in ipairs(cardList) do
-- 02014 = Duke (Ashcan Pete)
-- 03009 = Sophie (Mark Harrigan)
if (card.metadata.id == "02014" or card.metadata.id == "03009") then
card.zone = "BlankTop"
end
if card.metadata.startsInPlay then card.zone = "BlankTop" end
end
end
-- Check to see if the deck list has Ancestral Knowledge. If it does, move 5
-- random skills to SetAside3
-- Check to see if the deck list has Ancestral Knowledge. If it does, move 5 random skills to SetAside3
function handleAncestralKnowledge(cardList)
local hasAncestralKnowledge = false
local skillList = { }
-- Have to process the entire list to check for Ancestral Knowledge and get
-- all possible skills, so do both in one pass
local skillList = {}
-- Have to process the entire list to check for Ancestral Knowledge and get all possible skills, so do both in one pass
for i, card in ipairs(cardList) do
if (card.metadata.id == "07303") then
-- Ancestral Knowledge found
if card.metadata.id == "07303" then
hasAncestralKnowledge = true
card.zone = "SetAside3"
elseif (card.metadata.type == "Skill"
@ -518,8 +670,8 @@ function handleAncestralKnowledge(cardList)
table.insert(skillList, i)
end
end
if (hasAncestralKnowledge) then
for i = 1,5 do
if hasAncestralKnowledge then
for i = 1, 5 do
-- Move 5 random skills to SetAside3
local skillListIndex = math.random(#skillList)
cardList[skillList[skillListIndex]].zone = "UnderSetAside3"
@ -528,13 +680,95 @@ function handleAncestralKnowledge(cardList)
end
end
-- Test method. Loads all decks which were submitted to ArkhamDB on a given
-- date window.
-- Check for and handle Underworld Market by moving all Illicit cards to UnderSetAside3
---@param cardList: Deck list being created
---@param playerColor: Color this deck is being loaded for
function handleUnderworldMarket(cardList, playerColor)
local hasMarket = false
local illicitList = {}
-- Process the entire list to check for Underworld Market and get all possible skills, doing both in one pass
for i, card in ipairs(cardList) do
if card.metadata.id == "09077" then
-- Underworld Market found
hasMarket = true
card.zone = "SetAside3"
elseif (string.find(card.metadata.traits, "Illicit", 1, true)
and card.metadata.bonded_to == nil
and not card.metadata.weakness) then
table.insert(illicitList, i)
end
end
if hasMarket then
if #illicitList < 10 then
debugPrint("Only " .. #illicitList .. " Illicit cards in your deck, you can't trigger Underworld Market's ability."
, Priority.WARNING, playerColor)
else
-- Process cards to move them to the market deck. This is done in reverse
-- order because the sorting needs to be reversed (deck sorts for face down)
-- Performance here may be an issue, as table.remove() is an O(n) operation
-- which makes the full shift O(n^2). But keep it simple unless it becomes
-- a problem
for i = #illicitList, 1, -1 do
local moving = cardList[illicitList[i]]
moving.zone = "UnderSetAside3"
table.remove(cardList, illicitList[i])
table.insert(cardList, moving)
end
if #illicitList > 10 then
debugPrint("Moved all " .. #illicitList .. " Illicit cards to the Market deck, reduce it to 10", Priority.INFO,
playerColor)
else
debugPrint("Built the Market deck", Priority.INFO, playerColor)
end
end
end
end
-- If the investigator is Joe Diamond, extract all Insight events to SetAside5 to build the Hunch Deck.
---@param investigatorId: ID for the deck's investigator card. Passed separately because the investigator may not be included in the cardList
---@param cardList: Deck list being created
---@param playerColor: Color this deck is being loaded for
function handleHunchDeck(investigatorId, cardList, playerColor)
if investigatorId == "05002" then -- Joe Diamond
local insightList = {}
for i, card in ipairs(cardList) do
if (card.metadata.type == "Event"
and string.match(card.metadata.traits, "Insight")
and card.metadata.bonded_to == nil) then
table.insert(insightList, i)
end
end
-- Process insights to move them to the hunch deck. This is done in reverse
-- order because the sorting needs to be reversed (deck sorts for face down)
-- Performance here may be an issue, as table.remove() is an O(n) operation
-- which makes the full shift O(n^2). But keep it simple unless it becomes
-- a problem
for i = #insightList, 1, -1 do
local moving = cardList[insightList[i]]
moving.zone = "SetAside5"
table.remove(cardList, insightList[i])
table.insert(cardList, moving)
end
if #insightList < 11 then
debugPrint("Joe's hunch deck must have 11 cards but the deck only has " .. #insightList .. " Insight events.",
Priority.INFO, playerColor)
elseif #insightList > 11 then
debugPrint("Moved all " .. #insightList .. " Insight events to the hunch deck, reduce it to 11.", Priority.INFO,
playerColor)
else
debugPrint("Built Joe's hunch deck", Priority.INFO, playerColor)
end
end
end
-- Test method. Loads all decks which were submitted to ArkhamDB on a given date window.
function testLoadLotsOfDecks()
local configuration = getConfiguration()
local numDays = 7
local day = os.time{year=2021, month=7, day=15} -- Start date here
for i=1,numDays do
local day = os.time { year = 2021, month = 7, day = 15 } -- Start date here
for i = 1, numDays do
local dateString = os.date("%Y-%m-%d", day)
local deckList = Request.start({
configuration.api_uri,
@ -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,30 +800,30 @@ end
-- Start the deck build process for the given player color and deck ID. This
-- will retrieve the deck from ArkhamDB, and pass to a callback for processing.
-- Param playerColor String Color name of the player mat to place this deck
-- on (e.g. "Red")
-- Param deckId: ArkhamDB deck id to be loaded
---@param playerColor String Color name of the player mat to place this deck on (e.g. "Red")
---@param deckId: ArkhamDB deck id to be loaded
function buildDeck(playerColor, deckId)
local configuration = getConfiguration()
-- Get a simple card to see if the bag indexes are complete. If not, abort
-- the deck load. The called method will handle player notification.
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
local checkCard = allCardsBag.call("getCardById", { id = "01001"})
local checkCard = allCardsBag.call("getCardById", { id = "01001" })
if (checkCard ~= nil and checkCard.data == nil) then
return
end
local deckUri = { configuration.api_uri, getUiState().private and configuration.private_deck or configuration.public_deck, deckId }
local deckUri = { configuration.api_uri,
getUiState().private and configuration.private_deck or configuration.public_deck, deckId }
local deck = Request.start(deckUri, function (status)
local deck = Request.start(deckUri, function(status)
if string.find(status.text, "<!DOCTYPE html>") then
debugPrint("Private deck ID "..deckId.." is not shared", Priority.ERROR, playerColor)
return false, table.concat({ "Private deck ", deckId, " is not shared"})
debugPrint("Private deck ID " .. deckId .. " is not shared", Priority.ERROR, playerColor)
return false, table.concat({ "Private deck ", deckId, " is not shared" })
end
local json = JSON.decode(status.text)
if not json then
debugPrint("Deck ID "..deckId.." not found", Priority.ERROR, playerColor)
debugPrint("Deck ID " .. deckId .. " not found", Priority.ERROR, playerColor)
return false, "Deck not found!"
end
@ -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
@ -616,7 +849,7 @@ function Request:new(uri, configure)
setmetatable(this, self)
self.__index = self
if type(uri)=="table" then
if type(uri) == "table" then
uri = table.concat(uri, "/")
end
@ -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
@ -638,7 +871,7 @@ end
---@return Request
function Request.deferred(uri, on_success, on_error, ...)
local parameters = table.pack(...)
return Request:new(uri, function (request, status)
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
@ -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
@ -674,7 +907,7 @@ end
function Request.with_all(requests, on_success, on_error, ...)
local parameters = table.pack(...)
Wait.condition(function ()
Wait.condition(function()
---@type any[]
local results = {}
@ -689,16 +922,16 @@ function Request.with_all(requests, on_success, on_error, ...)
end
end
if (#errors<=0) then
if (#errors <= 0) then
on_success(results, table.unpack(parameters))
elseif on_error ==nil then
elseif on_error == nil then
for _, request in ipairs(errors) do
debugPrint(table.concat({ "[ERROR]", request.uri, ":", request.error_message }), Priority.ERROR)
end
else
on_error(requests, table.unpack(parameters))
end
end, function ()
end, function()
for _, request in ipairs(requests) do
if not request.is_done then return false end
end
@ -709,10 +942,10 @@ end
---@param callback fun(content: any, vararg any)
function Request:with(callback, ...)
local arguments = table.pack(...)
Wait.condition(function ()
Wait.condition(function()
if self.is_successful then
callback(self.content, table.unpack(arguments))
end
end, function () return self.is_done
end, function() return self.is_done
end)
end

View File

@ -1,15 +1,16 @@
local INPUT_FIELD_HEIGHT = 340
local INPUT_FIELD_WIDTH = 1500
local FIELD_COLOR = { 0.9, 0.7, 0.5 }
local FIELD_COLOR = {0.9,0.7,0.5}
local PRIVATE_TOGGLE_LABELS = { }
local PRIVATE_TOGGLE_LABELS = {}
PRIVATE_TOGGLE_LABELS[true] = "Private"
PRIVATE_TOGGLE_LABELS[false] = "Published"
local UPGRADED_TOGGLE_LABELS = { }
local UPGRADED_TOGGLE_LABELS = {}
UPGRADED_TOGGLE_LABELS[true] = "Upgraded"
UPGRADED_TOGGLE_LABELS[false] = "Specific"
local LOAD_INVESTIGATOR_TOGGLE_LABELS = { }
local LOAD_INVESTIGATOR_TOGGLE_LABELS = {}
LOAD_INVESTIGATOR_TOGGLE_LABELS[true] = "Yes"
LOAD_INVESTIGATOR_TOGGLE_LABELS[false] = "No"
@ -17,14 +18,13 @@ local redDeckId = ""
local orangeDeckId = ""
local whiteDeckId = ""
local greenDeckId = ""
local privateDeck = true
local loadNewestDeck = true
local loadInvestigators = false
local loadingColor = ""
-- Returns a table with the full state of the UI, including options and deck
-- IDs. This can be used to persist via onSave(), or provide values for a load
-- operation
-- Returns a table with the full state of the UI, including options and deck IDs.
-- This can be used to persist via onSave(), or provide values for a load operation
-- Table values:
-- redDeck: Deck ID to load for the red player
-- orangeDeck: Deck ID to load for the orange player
@ -41,14 +41,13 @@ function getUiState()
greenDeck = greenDeckId,
private = privateDeck,
loadNewest = loadNewestDeck,
investigators = loadInvestigators,
investigators = loadInvestigators
}
end
-- Sets up the UI for the deck loader, populating fields from the given save
-- state table decoded from onLoad()
-- Sets up the UI for the deck loader, populating fields from the given save state table decoded from onLoad()
function initializeUi(savedUiState)
if (savedUiState ~= nil) then
if savedUiState ~= nil then
redDeckId = savedUiState.redDeck
orangeDeckId = savedUiState.orangeDeck
whiteDeckId = savedUiState.whiteDeck
@ -56,14 +55,6 @@ function initializeUi(savedUiState)
privateDeck = savedUiState.private
loadNewestDeck = savedUiState.loadNewest
loadInvestigators = savedUiState.investigators
else
redDeckId = ""
orangeDeckId = ""
whiteDeckId = ""
greenDeckId = ""
privateDeck = true
loadNewestDeck = true
loadInvestigators = true
end
makeOptionToggles()
@ -72,59 +63,35 @@ function initializeUi(savedUiState)
end
function makeOptionToggles()
-- Creates the three option toggle buttons. Each toggle assumes its index as
-- part of the toggle logic. IF YOU CHANGE THE ORDER OF THESE FIELDS YOU MUST
-- CHANGE THE EVENT HANDLERS
makePublicPrivateToggle()
makeLoadUpgradedToggle()
makeLoadInvestigatorsToggle()
end
function makePublicPrivateToggle()
-- common parameters
local checkbox_parameters = {}
checkbox_parameters.function_owner = self
checkbox_parameters.width = INPUT_FIELD_WIDTH
checkbox_parameters.height = INPUT_FIELD_HEIGHT
checkbox_parameters.scale = { 0.1, 0.1, 0.1 }
checkbox_parameters.font_size = 240
checkbox_parameters.hover_color = { 0.4, 0.6, 0.8 }
checkbox_parameters.color = FIELD_COLOR
-- public / private deck
checkbox_parameters.click_function = "publicPrivateChanged"
checkbox_parameters.function_owner = self
checkbox_parameters.position = {0.25,0.1,-0.102}
checkbox_parameters.width = INPUT_FIELD_WIDTH
checkbox_parameters.height = INPUT_FIELD_HEIGHT
checkbox_parameters.tooltip = "Published or private deck.\n\n*****PLEASE USE A PRIVATE DECK IF JUST FOR TTS TO AVOID FLOODING ARKHAMDB PUBLISHED DECK LISTS!"
checkbox_parameters.position = { 0.25, 0.1, -0.102 }
checkbox_parameters.tooltip = "Published or private deck?\n\nPLEASE USE A PRIVATE DECK IF JUST FOR TTS TO AVOID FLOODING ARKHAMDB PUBLISHED DECK LISTS!"
checkbox_parameters.label = PRIVATE_TOGGLE_LABELS[privateDeck]
checkbox_parameters.font_size = 240
checkbox_parameters.scale = {0.1,0.1,0.1}
checkbox_parameters.color = FIELD_COLOR
checkbox_parameters.hover_color = {0.4,0.6,0.8}
self.createButton(checkbox_parameters)
end
function makeLoadUpgradedToggle()
local checkbox_parameters = {}
-- load upgraded?
checkbox_parameters.click_function = "loadUpgradedChanged"
checkbox_parameters.function_owner = self
checkbox_parameters.position = {0.25,0.1,-0.01}
checkbox_parameters.width = INPUT_FIELD_WIDTH
checkbox_parameters.height = INPUT_FIELD_HEIGHT
checkbox_parameters.tooltip = "Load newest upgrade, or exact deck"
checkbox_parameters.position = { 0.25, 0.1, -0.01 }
checkbox_parameters.tooltip = "Load newest upgrade or exact deck?"
checkbox_parameters.label = UPGRADED_TOGGLE_LABELS[loadNewestDeck]
checkbox_parameters.font_size = 240
checkbox_parameters.scale = {0.1,0.1,0.1}
checkbox_parameters.color = FIELD_COLOR
checkbox_parameters.hover_color = {0.4,0.6,0.8}
self.createButton(checkbox_parameters)
end
function makeLoadInvestigatorsToggle()
local checkbox_parameters = {}
-- load investigators?
checkbox_parameters.click_function = "loadInvestigatorsChanged"
checkbox_parameters.function_owner = self
checkbox_parameters.position = {0.25,0.1,0.081}
checkbox_parameters.width = INPUT_FIELD_WIDTH
checkbox_parameters.height = INPUT_FIELD_HEIGHT
checkbox_parameters.position = { 0.25, 0.1, 0.081 }
checkbox_parameters.tooltip = "Spawn investigator cards?"
checkbox_parameters.label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators]
checkbox_parameters.font_size = 240
checkbox_parameters.scale = {0.1,0.1,0.1}
checkbox_parameters.color = FIELD_COLOR
checkbox_parameters.hover_color = {0.4,0.6,0.8}
self.createButton(checkbox_parameters)
end
@ -133,103 +100,74 @@ function makeDeckIdFields()
local input_parameters = {}
-- Parameters common to all entry fields
input_parameters.function_owner = self
input_parameters.scale = {0.1,0.1,0.1}
input_parameters.scale = { 0.1, 0.1, 0.1 }
input_parameters.width = INPUT_FIELD_WIDTH
input_parameters.height = INPUT_FIELD_HEIGHT
input_parameters.font_size = 320
input_parameters.tooltip = "Deck ID from ArkhamDB URL of the deck\nPublic URL: 'https://arkhamdb.com/decklist/view/101/knowledge-overwhelming-solo-deck-1.0' = '101'\nPrivate URL: 'https://arkhamdb.com/deck/view/102' = '102'"
input_parameters.alignment = 3 -- Center
input_parameters.color = FIELD_COLOR
input_parameters.font_color = {0, 0, 0}
input_parameters.font_color = { 0, 0, 0 }
input_parameters.validation = 2 -- Integer
-- Green
input_parameters.input_function = "greenDeckChanged"
input_parameters.position = {-0.166,0.1,0.385}
input_parameters.value=greenDeckId
input_parameters.position = { -0.166, 0.1, 0.385 }
input_parameters.value = greenDeckId
self.createInput(input_parameters)
-- Red
input_parameters.input_function = "redDeckChanged"
input_parameters.position = {0.171,0.1,0.385}
input_parameters.value=redDeckId
input_parameters.position = { 0.171, 0.1, 0.385 }
input_parameters.value = redDeckId
self.createInput(input_parameters)
-- White
input_parameters.input_function = "whiteDeckChanged"
input_parameters.position = {-0.166,0.1,0.474}
input_parameters.value=whiteDeckId
input_parameters.position = { -0.166, 0.1, 0.474 }
input_parameters.value = whiteDeckId
self.createInput(input_parameters)
-- Orange
input_parameters.input_function = "orangeDeckChanged"
input_parameters.position = {0.171,0.1,0.474}
input_parameters.value=orangeDeckId
input_parameters.position = { 0.171, 0.1, 0.474 }
input_parameters.value = orangeDeckId
self.createInput(input_parameters)
end
-- Create the Build All button. This is a transparent button which covers the
-- Build All portion of the background graphic
-- Create the Build All button. This is a transparent button which covers the Build All portion of the background graphic
function makeBuildButton()
local button_parameters = {}
button_parameters.click_function = "loadDecks"
button_parameters.function_owner = self
button_parameters.position = {0,0.1,0.71}
button_parameters.position = { 0, 0.1, 0.71 }
button_parameters.width = 320
button_parameters.height = 30
button_parameters.color = {0, 0, 0, 0}
button_parameters.color = { 0, 0, 0, 0 }
button_parameters.tooltip = "Click to build all four decks!"
self.createButton(button_parameters)
end
-- Event handler for the Public/Private toggle. Changes the local value and the
-- labels to toggle the button
-- Event handlers for deck ID change
function redDeckChanged(_, _, inputValue) redDeckId = inputValue end
function orangeDeckChanged(_, _, inputValue) orangeDeckId = inputValue end
function whiteDeckChanged(_, _, inputValue) whiteDeckId = inputValue end
function greenDeckChanged(_, _, inputValue) greenDeckId = inputValue end
-- Event handlers for toggle buttons
function publicPrivateChanged()
-- editButton uses parameters.index which is 0-indexed
privateDeck = not privateDeck
self.editButton {
index = 0,
label = PRIVATE_TOGGLE_LABELS[privateDeck],
}
self.editButton { index = 0, label = PRIVATE_TOGGLE_LABELS[privateDeck] }
end
-- Event handler for the Upgraded toggle. Changes the local value and the
-- labels to toggle the button
function loadUpgradedChanged()
-- editButton uses parameters.index which is 0-indexed
loadNewestDeck = not loadNewestDeck
self.editButton {
index = 1,
label = UPGRADED_TOGGLE_LABELS[loadNewestDeck],
}
self.editButton { index = 1, label = UPGRADED_TOGGLE_LABELS[loadNewestDeck] }
end
-- Event handler for the load investigator cards toggle. Changes the local
-- value and the labels to toggle the button
function loadInvestigatorsChanged()
-- editButton uses parameters.index which is 0-indexed
loadInvestigators = not loadInvestigators
self.editButton {
index = 2,
label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators],
}
end
-- Event handler for deck ID change
function redDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected)
redDeckId = inputValue
end
-- Event handler for deck ID change
function orangeDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected)
orangeDeckId = inputValue
end
-- Event handler for deck ID change
function whiteDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected)
whiteDeckId = inputValue
end
-- Event handler for deck ID change
function greenDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected)
greenDeckId = inputValue
self.editButton { index = 2, label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators] }
end
function loadDecks()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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' } },

View 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

View File

@ -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

View File

@ -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
if not controlActive then return end
-- creates the label, input box and apply button
self.createButton({
function_owner = self,
label = "Playmat Image Swapper",
tooltip = "",
click_function = "none",
position = { 0, 0.15, 2.2 },
height = 0,
width = 0,
font_size = 300,
font_color = { 1, 1, 1 }
})
self.createInput({
function_owner = self,
label = "URL",
tooltip = "Enter URL for playmat image",
input_function = "none",
alignment = 3,
position = { 0, 0.15, 3 },
height = 323,
width = 4000,
font_size = 300
})
self.createButton({
function_owner = self,
label = "Apply Image\nTo Playmat",
tooltip = "Left-Click: Apply URL\nRight-Click: Reset to default image",
click_function = "click_applySurface",
position = { 0, 0.15, 4.1 },
height = 460,
width = 1400,
font_size = 200
})
end
-- click function for apply button
function click_applySurface(_, _, isRightClick)
if isRightClick then
updateSurface(defaultURL)
else
updateSurface(self.getInputs()[1].value)
end
end
-- input function for the input box
function none() end
--Table surface control
--Changes table surface
function click_applySurface(_, color)
if permissionCheck(color) then
updateSurface()
broadcastToAll("New Playmat Image Applied", {0.2,0.9,0.2})
end
end
--Updates surface from the values in the input field
function updateSurface()
-- 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

View File

@ -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
-- Constructs a list of available basic weaknesses by starting with the full pool of basic
-- weaknesses then removing any which are currently in the play or deck construction areas
-- Return: Table array of weakness IDs which are valid to choose from
function buildAvailableWeaknesses()
local weaknessesInPlay = { }
local allObjects = getAllObjects()
for _, object in ipairs(allObjects) do
if (object.name == "Deck" and isInPlayArea(object)) then
for _, cardData in ipairs(object.getData().ContainedObjects) do
local cardMetadata = JSON.decode(cardData.GMNotes)
incrementWeaknessCount(weaknessesInPlay, cardMetadata)
end
elseif (object.name == "Card" and isInPlayArea(object)) then
local cardMetadata = JSON.decode(object.getGMNotes())
incrementWeaknessCount(weaknessesInPlay, cardMetadata)
end
end
return weaknessId
local availableWeaknesses = { }
for _, weaknessId in ipairs(basicWeaknessList) do
if (weaknessesInPlay[weaknessId] ~= nil and weaknessesInPlay[weaknessId] > 0) then
weaknessesInPlay[weaknessId] = weaknessesInPlay[weaknessId] - 1
else
table.insert(availableWeaknesses, weaknessId)
end
end
return availableWeaknesses
end
-- Helper function that adds one to the table entry for the number of weaknesses in play
function incrementWeaknessCount(table, cardMetadata)
if (isBasicWeakness(cardMetadata)) then
if (table[cardMetadata.id] == nil) then
table[cardMetadata.id] = 1
else
table[cardMetadata.id] = table[cardMetadata.id] + 1
end
end
end
function isInPlayArea(object)
if (object == nil) then
return false
end
local position = object.getPosition()
return position.x < WEAKNESS_CHECK_X
and position.z < WEAKNESS_CHECK_Z
end
function isBasicWeakness(cardMetadata)
return cardMetadata ~= nil
and cardMetadata.weakness
and cardMetadata.basicWeaknessCount ~= nil
and cardMetadata.basicWeaknessCount > 0
end

View 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

View File

@ -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,

View File

@ -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
-- 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
end
drawCardsWithReshuffle(cardsToDraw)
end
if deckSize >= cardsToDraw then
drawCards(cardsToDraw)
return
end
drawCards(deckSize)
if discardPile ~= nil then
shuffleDiscardIntoDeck()
Wait.time(|| drawCards(cardsToDraw - deckSize), 1)
end
printToColor("Take 1 horror (drawing card from empty deck)", messageColor)
function forcedLearningDraw()
printToColor("Drawing 2 cards, discard 1 (Forced Learning)", messageColor)
drawCardsWithReshuffle(2)
end
function checkDeckZoneExists()
@ -368,6 +389,9 @@ end
-- spawn a group of tokens of the given type on the object
function spawnTokenGroup(object, tokenType, tokenCount)
if (tokenCount < 1 or tokenCount > 12) then
return
end
local offsets = PLAYER_CARD_TOKEN_OFFSETS[tokenCount]
if offsets == nil then
error("couldn't find offsets for " .. tokenCount .. ' tokens')
@ -392,8 +416,10 @@ function shouldSpawnTokens(object)
-- we assume we shouldn't spawn tokens if in doubt, this should
-- only ever happen on load and in that case prevents respawns
local spawned = DATA_HELPER.call('getSpawnedPlayerCardGuid', {object.getGUID()})
local canSpawn = getPlayerCardData(object)
return not spawned and canSpawn
local hasDataHelperData = getPlayerCardData(object)
local cardMetadata = JSON.decode(object.getGMNotes()) or {}
local hasUses = cardMetadata.uses ~= nil
return not spawned and (hasDataHelperData or hasUses)
end
function markSpawned(object)
@ -404,12 +430,32 @@ function markSpawned(object)
end
function spawnTokensFor(object)
local 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)

View File

@ -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

View File

@ -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

View File

@ -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

View 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
View 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

View File

@ -1,5 +1,5 @@
BLESS_COLOR = { r=0.3, g=0.25, b=0.09 }
CURSE_COLOR = { r=0.2, g=0.08, b=0.24 }
BLESS_COLOR = { r = 0.3, g = 0.25, b = 0.09 }
CURSE_COLOR = { r = 0.2, g = 0.08, b = 0.24 }
MIN_VALUE = 1
MAX_VALUE = 10
IMAGE_URL = {
@ -9,120 +9,122 @@ IMAGE_URL = {
function onload()
self.createButton({
label="Add",
click_function="addBlessToken",
function_owner=self,
position={-2.3,0.1,-0.5},
height=150,
width=300,
scale={x=1.75, y=1.75, z=1.75},
font_size=100,
font_color={ r=1, g=1, b=1 },
color=BLESS_COLOR
label = "Add",
click_function = "addBlessToken",
function_owner = self,
position = { -2.3, 0.1, -0.5 },
height = 150,
width = 300,
scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size = 100,
font_color = { r = 1, g = 1, b = 1 },
color = BLESS_COLOR
})
self.createButton({
label="Remove",
click_function="removeBlessToken",
function_owner=self,
position={-0.9,0.1,-0.5},
height=150,
width=450,
scale={x=1.75, y=1.75, z=1.75},
font_size=100,
font_color={ r=1, g=1, b=1 },
color=BLESS_COLOR
label = "Remove",
click_function = "removeBlessToken",
function_owner = self,
position = { -0.9, 0.1, -0.5 },
height = 150,
width = 450,
scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size = 100,
font_color = { r = 1, g = 1, b = 1 },
color = BLESS_COLOR
})
self.createButton({
label="Take",
click_function="takeBlessToken",
function_owner=self,
position={0.7,0.1,-0.5},
height=150,
width=350,
scale={x=1.75, y=1.75, z=1.75},
font_size=100,
font_color={ r=1, g=1, b=1 },
color=BLESS_COLOR
label = "Take",
click_function = "takeBlessToken",
function_owner = self,
position = { 0.7, 0.1, -0.5 },
height = 150,
width = 350,
scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size = 100,
font_color = { r = 1, g = 1, b = 1 },
color = BLESS_COLOR
})
self.createButton({
label="Return",
click_function="returnBlessToken",
function_owner=self,
position={2.1,0.1,-0.5},
height=150,
width=400,
scale={x=1.75, y=1.75, z=1.75},
font_size=100,
font_color={ r=1, g=1, b=1 },
color=BLESS_COLOR
label = "Return",
click_function = "returnBlessToken",
function_owner = self,
position = { 2.1, 0.1, -0.5 },
height = 150,
width = 400,
scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size = 100,
font_color = { r = 1, g = 1, b = 1 },
color = BLESS_COLOR
})
self.createButton({
label="Add",
click_function="addCurseToken",
function_owner=self,
position={-2.3,0.1,0.5},
height=150,
width=300,
scale={x=1.75, y=1.75, z=1.75},
font_size=100,
font_color={ r=1, g=1, b=1 },
color=CURSE_COLOR
label = "Add",
click_function = "addCurseToken",
function_owner = self,
position = { -2.3, 0.1, 0.5 },
height = 150,
width = 300,
scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size = 100,
font_color = { r = 1, g = 1, b = 1 },
color = CURSE_COLOR
})
self.createButton({
label="Remove",
click_function="removeCurseToken",
function_owner=self,
position={-0.9,0.1,0.5},
height=150,
width=450,
scale={x=1.75, y=1.75, z=1.75},
font_size=100,
font_color={ r=1, g=1, b=1 },
color=CURSE_COLOR
label = "Remove",
click_function = "removeCurseToken",
function_owner = self,
position = { -0.9, 0.1, 0.5 },
height = 150,
width = 450,
scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size = 100,
font_color = { r = 1, g = 1, b = 1 },
color = CURSE_COLOR
})
self.createButton({
label="Take",
click_function="takeCurseToken",
function_owner=self,
position={0.7,0.1,0.5},
height=150,
width=350,
scale={x=1.75, y=1.75, z=1.75},
font_size=100,
font_color={ r=1, g=1, b=1 },
color=CURSE_COLOR
label = "Take",
click_function = "takeCurseToken",
function_owner = self,
position = { 0.7, 0.1, 0.5 },
height = 150,
width = 350,
scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size = 100,
font_color = { r = 1, g = 1, b = 1 },
color = CURSE_COLOR
})
self.createButton({
label="Return",
click_function="returnCurseToken",
function_owner=self,
position={2.1,0.1,0.5},
height=150,
width=400,
scale={x=1.75, y=1.75, z=1.75},
font_size=100,
font_color={ r=1, g=1, b=1 },
color=CURSE_COLOR
label = "Return",
click_function = "returnCurseToken",
function_owner = self,
position = { 2.1, 0.1, 0.5 },
height = 150,
width = 400,
scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size = 100,
font_color = { r = 1, g = 1, b = 1 },
color = CURSE_COLOR
})
self.createButton({
label="Reset", click_function="doReset", function_owner=self,
position={0,0.3,1.8}, rotation={0,0,0}, height=350, width=800,
font_size=250, color={0,0,0}, font_color={1,1,1}
label = "Reset", click_function = "doReset", function_owner = self,
position = { 0, 0, 1.8 }, rotation = { 0, 0, 0 }, height = 350, width = 800,
font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
})
numInPlay = { Bless=0, Curse=0 }
tokensTaken = { Bless={}, Curse={} }
numInPlay = { Bless = 0, Curse = 0 }
tokensTaken = { Bless = {}, Curse = {} }
sealedTokens = {}
Wait.time(initializeState, 1)
addHotkey("Bless Curse Status", printStatus, false)
addHotkey("Wendy's Menu", addMenuOptions, false)
end
function initializeState()
@ -131,7 +133,7 @@ function initializeState()
local chaosbag = getChaosBag()
if chaosbag == nil then return end
local tokens = {}
for i,v in ipairs(chaosbag.getObjects()) do
for i, v in ipairs(chaosbag.getObjects()) do
if v.name == "Bless" then
numInPlay.Bless = numInPlay.Bless + 1
elseif v.name == "Curse" then
@ -140,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())
@ -174,8 +171,8 @@ end
function doReset(_obj, _color, alt_click)
playerColor = _color
numInPlay = { Bless=0, Curse=0 }
tokensTaken = { Bless={}, Curse={} }
numInPlay = { Bless = 0, Curse = 0 }
tokensTaken = { Bless = {}, Curse = {} }
initializeState()
end
@ -202,8 +199,8 @@ function spawnToken()
local url = IMAGE_URL[mode]
local obj = spawnObject({
type = 'Custom_Tile',
position = {pos.x, pos.y + 3, pos.z},
rotation = {x = 0, y = 260, z = 0},
position = { pos.x, pos.y + 3, pos.z },
rotation = { x = 0, y = 260, z = 0 },
callback_function = spawn_callback
})
obj.setCustomObject({
@ -211,7 +208,7 @@ function spawnToken()
image = url,
thickness = 0.10,
})
obj.scale {0.81, 1, 0.81}
obj.scale { 0.81, 1, 0.81 }
return obj
end
@ -242,8 +239,14 @@ function takeToken(type, _color, remove)
playerColor = _color
local chaosbag = getChaosBag()
if chaosbag == nil then return end
if not remove and not SEAL_CARD_MESSAGE then
broadcastToColor("Are you trying to seal a token on a card? Return " ..
"this one, then try right-clicking on the card for seal options.",
_color)
SEAL_CARD_MESSAGE = true
end
local tokens = {}
for i,v in ipairs(chaosbag.getObjects()) do
for i, v in ipairs(chaosbag.getObjects()) do
if v.name == type then
table.insert(tokens, v.guid)
end
@ -260,9 +263,13 @@ function takeToken(type, _color, remove)
end
local guid = table.remove(tokens)
mode = type
local position = Vector({ pos.x - 2, pos.y, pos.z + 2.5 })
if type == "Curse" then
position = position + Vector({ 0, 0, -5 })
end
chaosbag.takeObject({
guid = guid,
position = {pos.x-2, pos.y, pos.z},
position = position,
smooth = false,
callback_function = callback
})
@ -314,7 +321,7 @@ end
function getChaosBag()
local items = getObjectFromGUID("83ef06").getObjects()
local chaosbag = nil
for i,v in ipairs(items) do
for i, v in ipairs(items) do
if v.getDescription() == "Chaos Bag" then
chaosbag = getObjectFromGUID(v.getGUID())
break
@ -334,3 +341,104 @@ function getTokenCount()
return "(" .. (numInPlay[mode] - #tokensTaken[mode]) .. "/" ..
#tokensTaken[mode] .. ")"
end
function addMenuOptions(playerColor, hoveredObject, pointerPosition, isKeyUp)
local manager = self
if hoveredObject == nil or hoveredObject.getVar("MENU_ADDED") == true then return end
if hoveredObject.tag ~= "Card" then
broadcastToColor("Right-click seal options can only be added to cards", playerColor)
return
end
hoveredObject.addContextMenuItem("Seal Bless", function(color)
manager.call("sealToken", {
type = "Bless",
playerColor = color,
enemy = hoveredObject
})
end, true)
hoveredObject.addContextMenuItem("Release Bless", function(color)
manager.call("releaseToken", {
type = "Bless",
playerColor = color,
enemy = hoveredObject
})
end, true)
hoveredObject.addContextMenuItem("Seal Curse", function(color)
manager.call("sealToken", {
type = "Curse",
playerColor = color,
enemy = hoveredObject
})
end, true)
hoveredObject.addContextMenuItem("Release Curse", function(color)
manager.call("releaseToken", {
type = "Curse",
playerColor = color,
enemy = hoveredObject
})
end, true)
broadcastToColor("Right-click seal options added to " .. hoveredObject.getName(), playerColor)
hoveredObject.setVar("MENU_ADDED", true)
sealedTokens[hoveredObject.getGUID()] = {}
end
function sealToken(params)
playerColor = params.playerColor
local chaosbag = getChaosBag()
if chaosbag == nil then return end
local pos = params.enemy.getPosition()
local manager = self
for i, token in ipairs(chaosbag.getObjects()) do
if token.name == params.type then
chaosbag.takeObject({
position = { pos.x, pos.y + 1, pos.z },
index = i - 1,
smooth = false,
callback_function = function(obj)
Wait.frames(function()
local mSealedTokens = manager.getVar("sealedTokens")
local tokens = mSealedTokens[params.enemy.getGUID()]
table.insert(tokens, obj)
manager.setVar("sealedTokens", mSealedTokens)
local guid = obj.getGUID()
local tokensTaken = manager.getVar("tokensTaken")
table.insert(tokensTaken[params.type], guid)
manager.setVar("tokensTaken", tokensTaken)
manager.setVar("mode", params.type)
printToColor("Sealing " .. params.type .. " token " .. manager.call("getTokenCount"),
params.playerColor)
end
, 1)
end
})
return
end
end
printToColor(params.type .. " token not found in bag", playerColor)
end
function releaseToken(params)
playerColor = params.playerColor
local chaosbag = getChaosBag()
if chaosbag == nil then return end
local tokens = sealedTokens[params.enemy.getGUID()]
if tokens == nil or #tokens == 0 then return end
mode = params.type
for i, token in ipairs(tokens) do
if token ~= nil and token.getName() == params.type then
local guid = token.getGUID()
chaosbag.putObject(token)
for j, v in ipairs(tokensTaken[mode]) do
if v == guid then
table.remove(tokensTaken[mode], j)
table.remove(tokens, i)
printToColor("Releasing " .. mode .. " token" .. getTokenCount(), params.playerColor)
return
end
end
end
end
printToColor(params.type .. " token not sealed on " .. params.enemy.getName(), params.playerColor)
end

View 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

View File

@ -1,5 +1,5 @@
function updateSave()
local data_to_save = {["ml"]=memoryList}
local data_to_save = { ["ml"] = memoryList }
saved_data = JSON.encode(data_to_save)
self.script_state = saved_data
end
@ -21,16 +21,20 @@ function onload(saved_data)
end
end
--Beginning Setup
--Make setup button
function createSetupButton()
self.createButton({
label="Setup", click_function="buttonClick_setup", function_owner=self,
position={0,5,-2}, rotation={0,0,0}, height=250, width=600,
font_size=150, color={0,0,0}, font_color={1,1,1}
label = "Setup",
click_function = "buttonClick_setup",
function_owner = self,
position = { 0, 5, -2 },
rotation = { 0, 0, 0 },
height = 250,
width = 600,
font_size = 150,
color = { 0, 0, 0 },
font_color = { 1, 1, 1 }
})
end
@ -50,7 +54,7 @@ function createButtonsOnAllObjects()
if obj ~= self then
local dummyIndex = howManyButtons
--On a normal bag, the button positions aren't the same size as the bag.
globalScaleFactor = 1.25 * 1/self.getScale().x
globalScaleFactor = 1.25 * 1 / self.getScale().x
--Super sweet math to set button positions
local selfPos = self.getPosition()
local objPos = obj.getPosition()
@ -67,9 +71,9 @@ function createButtonsOnAllObjects()
local func = function() buttonClick_selection(dummyIndex, obj) end
self.setVar(funcName, func)
self.createButton({
click_function=funcName, function_owner=self,
position=objPos, rotation=rot, height=1000, width=1000,
color={0.75,0.25,0.25,0.6},
click_function = funcName, function_owner = self,
position = objPos, rotation = rot, height = 1000, width = 1000,
color = { 0.75, 0.25, 0.25, 0.6 },
})
howManyButtons = howManyButtons + 1
end
@ -79,43 +83,40 @@ end
--Creates submit and cancel buttons
function createSetupActionButtons()
self.createButton({
label="Cancel", click_function="buttonClick_cancel", function_owner=self,
position={1.5,5,2}, rotation={0,0,0}, height=350, width=1100,
font_size=250, color={0,0,0}, font_color={1,1,1}
label = "Cancel", click_function = "buttonClick_cancel", function_owner = self,
position = { 1.5, 5, 2 }, rotation = { 0, 0, 0 }, height = 350, width = 1100,
font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
})
self.createButton({
label="Submit", click_function="buttonClick_submit", function_owner=self,
position={-1.2,5,2}, rotation={0,0,0}, height=350, width=1100,
font_size=250, color={0,0,0}, font_color={1,1,1}
label = "Submit", click_function = "buttonClick_submit", function_owner = self,
position = { -1.2, 5, 2 }, rotation = { 0, 0, 0 }, height = 350, width = 1100,
font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
})
self.createButton({
label="Reset", click_function="buttonClick_reset", function_owner=self,
position={-3.5,5,2}, rotation={0,0,0}, height=350, width=800,
font_size=250, color={0,0,0}, font_color={1,1,1}
label = "Reset", click_function = "buttonClick_reset", function_owner = self,
position = { -3.5, 5, 2 }, rotation = { 0, 0, 0 }, height = 350, width = 800,
font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
})
end
--During Setup
--Checks or unchecks buttons
function buttonClick_selection(index, obj)
local color = {0,1,0,0.6}
local color = { 0, 1, 0, 0.6 }
if memoryList[obj.getGUID()] == nil then
self.editButton({index=index, color=color})
self.editButton({ index = index, color = color })
--Adding pos/rot to memory table
local pos, rot = obj.getPosition(), obj.getRotation()
--I need to add it like this or it won't save due to indexing issue
memoryList[obj.getGUID()] = {
pos={x=round(pos.x,4), y=round(pos.y,4), z=round(pos.z,4)},
rot={x=round(rot.x,4), y=round(rot.y,4), z=round(rot.z,4)},
lock=obj.getLock()
pos = { x = round(pos.x, 4), y = round(pos.y, 4), z = round(pos.z, 4) },
rot = { x = round(rot.x, 4), y = round(rot.y, 4), z = round(rot.z, 4) },
lock = obj.getLock()
}
obj.highlightOn({0,1,0})
obj.highlightOn({ 0, 1, 0 })
else
color = {0.75,0.25,0.25,0.6}
self.editButton({index=index, color=color})
color = { 0.75, 0.25, 0.25, 0.6 }
self.editButton({ index = index, color = color })
memoryList[obj.getGUID()] = nil
obj.highlightOff()
end
@ -131,13 +132,13 @@ function buttonClick_cancel()
createMemoryActionButtons()
end
removeAllHighlights()
broadcastToAll("Selection Canceled", {1,1,1})
broadcastToAll("Selection Canceled", { 1, 1, 1 })
end
--Saves selections
function buttonClick_submit()
if next(memoryList) == nil then
broadcastToAll("You cannot submit without any selections.", {0.75, 0.25, 0.25})
broadcastToAll("You cannot submit without any selections.", { 0.75, 0.25, 0.25 })
else
self.clearButtons()
createMemoryActionButtons()
@ -147,7 +148,7 @@ function buttonClick_submit()
local obj = getObjectFromGUID(guid)
if obj ~= nil then obj.highlightOff() end
end
broadcastToAll(count.." Objects Saved", {1,1,1})
broadcastToAll(count .. " Objects Saved", { 1, 1, 1 })
updateSave()
end
end
@ -158,31 +159,55 @@ function buttonClick_reset()
self.clearButtons()
createSetupButton()
removeAllHighlights()
broadcastToAll("Tool Reset", {1,1,1})
broadcastToAll("Tool Reset", { 1, 1, 1 })
updateSave()
end
--After Setup
--Creates recall and place buttons
function createMemoryActionButtons()
self.createButton({
label="Clicker", click_function="buttonClick_place", function_owner=self,
position={4.2,1,0}, rotation={0,0,0}, height=500, width=1100,
font_size=350, color={0,0,0}, font_color={1,1,1}
label = "Clicker", click_function = "buttonClick_place", function_owner = self,
position = { 4.2, 1, 0 }, rotation = { 0, 0, 0 }, height = 500, width = 1100,
font_size = 350, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
})
self.createButton({
label="Counter", click_function="buttonClick_recall", function_owner=self,
position={-4.2,1,-0.1}, rotation={0,0,0}, height=500, width=1300,
label = "Counter", click_function = "buttonClick_recall", function_owner = self,
position = { -4.2, 1, -0.1 }, rotation = { 0, 0, 0 }, height = 500, width = 1300,
font_size = 350, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
})
self.createButton({
label = "Add Draw 1 Buttons", click_function = "addDraw1Buttons", function_owner = self,
position = { 0, 1, -2.5 }, rotation = { 0, 0, 0 }, height = 500, width = 2600,
font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
})
--[[
self.createButton({
label="Setup", click_function="buttonClick_setup", function_owner=self,
position={-6,1,0}, rotation={0,90,0}, height=500, width=1200,
font_size=350, color={0,0,0}, font_color={1,1,1}
})
-- self.createButton({
-- label="Setup", click_function="buttonClick_setup", function_owner=self,
-- position={-6,1,0}, rotation={0,90,0}, height=500, width=1200,
-- font_size=350, color={0,0,0}, font_color={1,1,1}
-- })
--]]
end
function addDraw1Buttons()
if ADD_BUTTONS_DISABLED then return end
local mats = { "8b081b", "bd0ff4", "383d8b", "0840d5" }
for i, guid in ipairs(mats) do
local mat = getObjectFromGUID(guid)
mat.createButton({
label = "Draw 1",
click_function = "doDrawOne",
function_owner = mat,
position = { 1.84, 0.1, -0.36 },
scale = { 0.12, 0.12, 0.12 },
width = 800,
height = 280,
font_size = 180
})
end
ADD_BUTTONS_DISABLED = true
end
--Sends objects from bag/table to their saved position/rotation
@ -200,7 +225,7 @@ function buttonClick_place()
for _, bagObj in ipairs(bagObjList) do
if bagObj.guid == guid then
local item = self.takeObject({
guid=guid, position=entry.pos, rotation=entry.rot,
guid = guid, position = entry.pos, rotation = entry.rot,
})
item.setLock(entry.lock)
break
@ -208,7 +233,7 @@ function buttonClick_place()
end
end
end
broadcastToAll("Objects Placed", {1,1,1})
broadcastToAll("Objects Placed", { 1, 1, 1 })
end
--Recalls objects to bag from table
@ -217,20 +242,17 @@ function buttonClick_recall()
local obj = getObjectFromGUID(guid)
if obj ~= nil then self.putObject(obj) end
end
broadcastToAll("Objects Recalled", {1,1,1})
broadcastToAll("Objects Recalled", { 1, 1, 1 })
end
--Utility functions
--Find delta (difference) between 2 x/y/z coordinates
function findOffsetDistance(p1, p2, obj)
local deltaPos = {}
local bounds = obj.getBounds()
deltaPos.x = (p2.x-p1.x)
deltaPos.y = (p2.y-p1.y) + (bounds.size.y - bounds.offset.y)
deltaPos.z = (p2.z-p1.z)
deltaPos.x = (p2.x - p1.x)
deltaPos.y = (p2.y - p1.y) + (bounds.size.y - bounds.offset.y)
deltaPos.z = (p2.z - p1.z)
return deltaPos
end
@ -240,8 +262,7 @@ 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}
return { x = x, y = desiredPos.y, z = z }
end
--Coroutine delay, in seconds
@ -268,6 +289,6 @@ end
--Round number (num) to the Nth decimal (dec)
function round(num, dec)
local mult = 10^(dec or 0)
local mult = 10 ^ (dec or 0)
return math.floor(num * mult + 0.5) / mult
end

View 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

View 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

View File

@ -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

View File

@ -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>