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

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,16 @@
local INPUT_FIELD_HEIGHT = 340 local INPUT_FIELD_HEIGHT = 340
local INPUT_FIELD_WIDTH = 1500 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[true] = "Private"
PRIVATE_TOGGLE_LABELS[false] = "Published" PRIVATE_TOGGLE_LABELS[false] = "Published"
local UPGRADED_TOGGLE_LABELS = { }
local UPGRADED_TOGGLE_LABELS = {}
UPGRADED_TOGGLE_LABELS[true] = "Upgraded" UPGRADED_TOGGLE_LABELS[true] = "Upgraded"
UPGRADED_TOGGLE_LABELS[false] = "Specific" 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[true] = "Yes"
LOAD_INVESTIGATOR_TOGGLE_LABELS[false] = "No" LOAD_INVESTIGATOR_TOGGLE_LABELS[false] = "No"
@ -17,14 +18,13 @@ local redDeckId = ""
local orangeDeckId = "" local orangeDeckId = ""
local whiteDeckId = "" local whiteDeckId = ""
local greenDeckId = "" local greenDeckId = ""
local privateDeck = true local privateDeck = true
local loadNewestDeck = true local loadNewestDeck = true
local loadInvestigators = false local loadInvestigators = false
local loadingColor = ""
-- Returns a table with the full state of the UI, including options and deck -- Returns a table with the full state of the UI, including options and deck IDs.
-- IDs. This can be used to persist via onSave(), or provide values for a load -- This can be used to persist via onSave(), or provide values for a load operation
-- operation
-- Table values: -- Table values:
-- redDeck: Deck ID to load for the red player -- redDeck: Deck ID to load for the red player
-- orangeDeck: Deck ID to load for the orange player -- orangeDeck: Deck ID to load for the orange player
@ -41,14 +41,13 @@ function getUiState()
greenDeck = greenDeckId, greenDeck = greenDeckId,
private = privateDeck, private = privateDeck,
loadNewest = loadNewestDeck, loadNewest = loadNewestDeck,
investigators = loadInvestigators, investigators = loadInvestigators
} }
end end
-- Sets up the UI for the deck loader, populating fields from the given save -- Sets up the UI for the deck loader, populating fields from the given save state table decoded from onLoad()
-- state table decoded from onLoad()
function initializeUi(savedUiState) function initializeUi(savedUiState)
if (savedUiState ~= nil) then if savedUiState ~= nil then
redDeckId = savedUiState.redDeck redDeckId = savedUiState.redDeck
orangeDeckId = savedUiState.orangeDeck orangeDeckId = savedUiState.orangeDeck
whiteDeckId = savedUiState.whiteDeck whiteDeckId = savedUiState.whiteDeck
@ -56,14 +55,6 @@ function initializeUi(savedUiState)
privateDeck = savedUiState.private privateDeck = savedUiState.private
loadNewestDeck = savedUiState.loadNewest loadNewestDeck = savedUiState.loadNewest
loadInvestigators = savedUiState.investigators loadInvestigators = savedUiState.investigators
else
redDeckId = ""
orangeDeckId = ""
whiteDeckId = ""
greenDeckId = ""
privateDeck = true
loadNewestDeck = true
loadInvestigators = true
end end
makeOptionToggles() makeOptionToggles()
@ -72,59 +63,35 @@ function initializeUi(savedUiState)
end end
function makeOptionToggles() function makeOptionToggles()
-- Creates the three option toggle buttons. Each toggle assumes its index as -- common parameters
-- 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()
local checkbox_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.click_function = "publicPrivateChanged"
checkbox_parameters.function_owner = self checkbox_parameters.position = { 0.25, 0.1, -0.102 }
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.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.label = PRIVATE_TOGGLE_LABELS[privateDeck] 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) self.createButton(checkbox_parameters)
end
function makeLoadUpgradedToggle() -- load upgraded?
local checkbox_parameters = {}
checkbox_parameters.click_function = "loadUpgradedChanged" checkbox_parameters.click_function = "loadUpgradedChanged"
checkbox_parameters.function_owner = self checkbox_parameters.position = { 0.25, 0.1, -0.01 }
checkbox_parameters.position = {0.25,0.1,-0.01} checkbox_parameters.tooltip = "Load newest upgrade or exact deck?"
checkbox_parameters.width = INPUT_FIELD_WIDTH
checkbox_parameters.height = INPUT_FIELD_HEIGHT
checkbox_parameters.tooltip = "Load newest upgrade, or exact deck"
checkbox_parameters.label = UPGRADED_TOGGLE_LABELS[loadNewestDeck] 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) self.createButton(checkbox_parameters)
end
function makeLoadInvestigatorsToggle() -- load investigators?
local checkbox_parameters = {}
checkbox_parameters.click_function = "loadInvestigatorsChanged" checkbox_parameters.click_function = "loadInvestigatorsChanged"
checkbox_parameters.function_owner = self checkbox_parameters.position = { 0.25, 0.1, 0.081 }
checkbox_parameters.position = {0.25,0.1,0.081}
checkbox_parameters.width = INPUT_FIELD_WIDTH
checkbox_parameters.height = INPUT_FIELD_HEIGHT
checkbox_parameters.tooltip = "Spawn investigator cards?" checkbox_parameters.tooltip = "Spawn investigator cards?"
checkbox_parameters.label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators] 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) self.createButton(checkbox_parameters)
end end
@ -133,103 +100,74 @@ function makeDeckIdFields()
local input_parameters = {} local input_parameters = {}
-- Parameters common to all entry fields -- Parameters common to all entry fields
input_parameters.function_owner = self 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.width = INPUT_FIELD_WIDTH
input_parameters.height = INPUT_FIELD_HEIGHT input_parameters.height = INPUT_FIELD_HEIGHT
input_parameters.font_size = 320 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.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.alignment = 3 -- Center
input_parameters.color = FIELD_COLOR input_parameters.color = FIELD_COLOR
input_parameters.font_color = {0, 0, 0} input_parameters.font_color = { 0, 0, 0 }
input_parameters.validation = 2 -- Integer input_parameters.validation = 2 -- Integer
-- Green -- Green
input_parameters.input_function = "greenDeckChanged" input_parameters.input_function = "greenDeckChanged"
input_parameters.position = {-0.166,0.1,0.385} input_parameters.position = { -0.166, 0.1, 0.385 }
input_parameters.value=greenDeckId input_parameters.value = greenDeckId
self.createInput(input_parameters) self.createInput(input_parameters)
-- Red -- Red
input_parameters.input_function = "redDeckChanged" input_parameters.input_function = "redDeckChanged"
input_parameters.position = {0.171,0.1,0.385} input_parameters.position = { 0.171, 0.1, 0.385 }
input_parameters.value=redDeckId input_parameters.value = redDeckId
self.createInput(input_parameters) self.createInput(input_parameters)
-- White -- White
input_parameters.input_function = "whiteDeckChanged" input_parameters.input_function = "whiteDeckChanged"
input_parameters.position = {-0.166,0.1,0.474} input_parameters.position = { -0.166, 0.1, 0.474 }
input_parameters.value=whiteDeckId input_parameters.value = whiteDeckId
self.createInput(input_parameters) self.createInput(input_parameters)
-- Orange -- Orange
input_parameters.input_function = "orangeDeckChanged" input_parameters.input_function = "orangeDeckChanged"
input_parameters.position = {0.171,0.1,0.474} input_parameters.position = { 0.171, 0.1, 0.474 }
input_parameters.value=orangeDeckId input_parameters.value = orangeDeckId
self.createInput(input_parameters) self.createInput(input_parameters)
end end
-- Create the Build All button. This is a transparent button which covers the -- Create the Build All button. This is a transparent button which covers the Build All portion of the background graphic
-- Build All portion of the background graphic
function makeBuildButton() function makeBuildButton()
local button_parameters = {} local button_parameters = {}
button_parameters.click_function = "loadDecks" button_parameters.click_function = "loadDecks"
button_parameters.function_owner = self 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.width = 320
button_parameters.height = 30 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!" button_parameters.tooltip = "Click to build all four decks!"
self.createButton(button_parameters) self.createButton(button_parameters)
end end
-- Event handler for the Public/Private toggle. Changes the local value and the -- Event handlers for deck ID change
-- labels to toggle the button 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() function publicPrivateChanged()
-- editButton uses parameters.index which is 0-indexed
privateDeck = not privateDeck privateDeck = not privateDeck
self.editButton { self.editButton { index = 0, label = PRIVATE_TOGGLE_LABELS[privateDeck] }
index = 0,
label = PRIVATE_TOGGLE_LABELS[privateDeck],
}
end end
-- Event handler for the Upgraded toggle. Changes the local value and the
-- labels to toggle the button
function loadUpgradedChanged() function loadUpgradedChanged()
-- editButton uses parameters.index which is 0-indexed
loadNewestDeck = not loadNewestDeck loadNewestDeck = not loadNewestDeck
self.editButton { self.editButton { index = 1, label = UPGRADED_TOGGLE_LABELS[loadNewestDeck] }
index = 1,
label = UPGRADED_TOGGLE_LABELS[loadNewestDeck],
}
end end
-- Event handler for the load investigator cards toggle. Changes the local
-- value and the labels to toggle the button
function loadInvestigatorsChanged() function loadInvestigatorsChanged()
-- editButton uses parameters.index which is 0-indexed
loadInvestigators = not loadInvestigators loadInvestigators = not loadInvestigators
self.editButton { self.editButton { index = 2, label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators] }
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
end end
function loadDecks() 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() function buttonClick_draw()
local allCardsBag = getObjectFromGUID(allCardsBagGuid) local allCardsBag = getObjectFromGUID(allCardsBagGuid)
local weaknessId = allCardsBag.call("getRandomWeaknessId") 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 }) local card = allCardsBag.call("getCardById", { id = weaknessId })
spawnObjectData({ spawnObjectData({
data = card.data, 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"}, "Wine Cellar_b882f3": {"type": "perPlayer", "value": 2, "clueSide": "back"},
"Hidden Passageway": {"type": "perPlayer", "value": 1, "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"}, "XXXX": {"type": "fixed", "value": 2, "clueSide": "back"},
"xxx": {"type": "perPlayer", "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 = [[ PLAYER_CARD_DATA_JSON = [[
{ {
"Flashlight": { "xxx": {
"tokenType": "resource", "tokenType": "resource",
"tokenCount": 3 "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", "tokenType": "resource",
"tokenCount": 3 "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' } } 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 -----------------The Side Missions
--official --official
['Curse of the Rougarou'] = { ['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' } } 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 --fan-made
['Carnevale of Spiders'] = { ['Carnevale of Spiders'] = {
normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }, 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 = { TOKEN_DATA = {
clue = {image = tokenplayerone.clue, scale = {0.15, 0.15, 0.15}}, clue = {image = tokenplayerone.clue, scale = {0.15, 0.15, 0.15}},
resource = {image = tokenplayerone.resource, scale = {0.17, 0.17, 0.17}}, 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 getObjectFromGUID("6161b4").interactable=false
@ -49,6 +51,7 @@ function onload()
getObjectFromGUID("9487a4").interactable=false getObjectFromGUID("9487a4").interactable=false
getObjectFromGUID("91dd9b").interactable=false getObjectFromGUID("91dd9b").interactable=false
getObjectFromGUID("f182ee").interactable=false getObjectFromGUID("f182ee").interactable=false
getObjectFromGUID("7bff34").interactable=false
end end
@ -134,6 +137,8 @@ PULLS = {
["http://cloud-3.steamusercontent.com/ugc/1655601092778627699/339FB716CB25CA6025C338F13AFDFD9AC6FA8356/"] = 0, ["http://cloud-3.steamusercontent.com/ugc/1655601092778627699/339FB716CB25CA6025C338F13AFDFD9AC6FA8356/"] = 0,
-- elder sign -- elder sign
["https://i.imgur.com/nEmqjmj.png"] = 0, ["https://i.imgur.com/nEmqjmj.png"] = 0,
-- frost
["http://cloud-3.steamusercontent.com/ugc/1858293462583104677/195F93C063A8881B805CE2FD4767A9718B27B6AE/"] = 0,
} }
IMAGE_TOKEN_MAP = { IMAGE_TOKEN_MAP = {
@ -172,7 +177,9 @@ IMAGE_TOKEN_MAP = {
-- bless -- bless
["http://cloud-3.steamusercontent.com/ugc/1655601092778627699/339FB716CB25CA6025C338F13AFDFD9AC6FA8356/"] = "Bless", ["http://cloud-3.steamusercontent.com/ugc/1655601092778627699/339FB716CB25CA6025C338F13AFDFD9AC6FA8356/"] = "Bless",
-- curse -- 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() function resetStats()
@ -686,6 +693,7 @@ function getImageUrl(id)
if id == 'elder' then return 'https://i.imgur.com/ttnspKt.png' end 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 == 'red' then return 'https://i.imgur.com/lns4fhz.png' end
if id == 'blue' then return 'https://i.imgur.com/nEmqjmj.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 '' return ''
end end
@ -766,3 +774,199 @@ function updateRandomSeed()
math.randomseed(os.time()) math.randomseed(os.time())
end end
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
function onSave() -- original by: -
saved_data = JSON.encode({tid=tableImageData, cd=checkData}) -- description: changes the big playmats image
--saved_data = "" information = {
return saved_data version = "1.1",
end last_updated = "10.10.2022"
function onload(saved_data)
--Loads the tracking for if the game has started yet
if saved_data ~= "" then
local loaded_data = JSON.decode(saved_data)
tableImageData = loaded_data.tid
checkData = loaded_data.cd
else
tableImageData = {}
checkData = {move=false, scale=false}
end
--Disables interactable status of objects with GUID in list
for _, guid in ipairs(ref_noninteractable) do
local obj = getObjectFromGUID(guid)
if obj then obj.interactable = false end
end
obj_surface = getObjectFromGUID("721ba2")
controlActive = false
createOpenCloseButton()
end
--Activation/deactivation of control panel
--Activated by clicking on
function click_toggleControl(_, color)
if permissionCheck(color) then
if not controlActive then
--Activate control panel
controlActive = true
self.clearButtons()
createOpenCloseButton()
createSurfaceInput()
createSurfaceButtons()
else
--Deactivate control panel
controlActive = false
self.clearButtons()
self.clearInputs()
createOpenCloseButton()
end
end
end
--Table surface control
--Changes table surface
function click_applySurface(_, color)
if permissionCheck(color) then
updateSurface()
broadcastToAll("New Playmat Image Applied", {0.2,0.9,0.2})
end
end
--Updates surface from the values in the input field
function updateSurface()
local customInfo = obj_surface.getCustomObject()
customInfo.image = self.getInputs()[1].value
obj_surface.setCustomObject(customInfo)
obj_surface = obj_surface.reload()
end
--Information gathering
--Checks if a color is promoted or host
function permissionCheck(color)
if Player[color].host==true or Player[color].promoted==true then
return true
else
return false
end
end
--Locates a string saved within memory file
function findInImageDataIndex(...)
for _, str in ipairs({...}) do
for i, v in ipairs(tableImageData) do
if v.url == str or v.name == str then
return i
end
end
end
return nil
end
--Round number (num) to the Nth decimal (dec)
function round(num, dec)
local mult = 10^(dec or 0)
return math.floor(num * mult + 0.5) / mult
end
--Locates a button with a helper function
function findButton(obj, func)
if func==nil then error("No func supplied to findButton") end
for _, v in ipairs(obj.getButtons()) do
if func(v) then
return v
end
end
return nil
end
--Creation of buttons/inputs
function createOpenCloseButton()
local tooltip = "Open Playmat Panel"
if controlActive then
tooltip = "Close Playmat Panel"
end
self.createButton({
click_function="click_toggleControl", function_owner=self,
position={0,0,0}, rotation={-45,0,0}, height=1500, width=1500,
color={1,1,1,0}, tooltip=tooltip
})
end
function createSurfaceInput()
local currentURL = obj_surface.getCustomObject().diffuse
local nickname = ""
if findInImageDataIndex(currentURL) ~= nil then
nickname = tableImageData[findInImageDataIndex(currentURL)].name
end
self.createInput({
label="URL", input_function="none", function_owner=self,
alignment=3, position={0,0.15,3}, height=224, width=4000,
font_size=200, tooltip="Enter URL for playmat image",
value=currentURL
})
end
function createSurfaceButtons()
--Label
self.createButton({
label="Playmat Image Swapper", click_function="none",
position={0,0.15,2.2}, height=0, width=0, font_size=300, font_color={1,1,1}
})
--Functional
self.createButton({
label="Apply Image\nTo Playmat", click_function="click_applySurface",
function_owner=self, tooltip="Apply URL as playmat image",
position={0,0.15,4}, height=440, width=1400, font_size=200,
})
end
--Data tables
ref_noninteractable = {
"afc863","c8edca","393bf7","12c65e","f938a2","9f95fd","35b95f",
"5af8f2","4ee1f2","bd69bd"
} }
ref_playerColor = { defaultURL = "http://cloud-3.steamusercontent.com/ugc/998015670465071049/FFAE162920D67CF38045EFBD3B85AD0F916147B2/"
"White", "Brown", "Red", "Orange", "Yellow",
"Green", "Teal", "Blue", "Purple", "Pink", "Black"
}
--Dummy function, absorbs unwanted triggers -- parameters for open/close button for reusing
BUTTON_PARAMETERS = {}
BUTTON_PARAMETERS.function_owner = self
BUTTON_PARAMETERS.click_function = "click_toggleControl"
BUTTON_PARAMETERS.height = 1500
BUTTON_PARAMETERS.width = 1500
BUTTON_PARAMETERS.color = { 1, 1, 1, 0 }
function onload()
controlActive = false
createOpenCloseButton()
self.addContextMenuItem("More Information", function()
printToAll("------------------------------", "White")
printToAll("Playmat Image Swapper v" .. information["version"] .. " by Chr1Z", "Orange")
printToAll("last updated: " .. information["last_updated"], "White")
printToAll("Original made by unknown", "White")
end)
end
-- click function for main button
function click_toggleControl()
self.clearButtons()
self.clearInputs()
controlActive = not controlActive
createOpenCloseButton()
if not controlActive then return end
-- creates the label, input box and apply button
self.createButton({
function_owner = self,
label = "Playmat Image Swapper",
tooltip = "",
click_function = "none",
position = { 0, 0.15, 2.2 },
height = 0,
width = 0,
font_size = 300,
font_color = { 1, 1, 1 }
})
self.createInput({
function_owner = self,
label = "URL",
tooltip = "Enter URL for playmat image",
input_function = "none",
alignment = 3,
position = { 0, 0.15, 3 },
height = 323,
width = 4000,
font_size = 300
})
self.createButton({
function_owner = self,
label = "Apply Image\nTo Playmat",
tooltip = "Left-Click: Apply URL\nRight-Click: Reset to default image",
click_function = "click_applySurface",
position = { 0, 0.15, 4.1 },
height = 460,
width = 1400,
font_size = 200
})
end
-- click function for apply button
function click_applySurface(_, _, isRightClick)
if isRightClick then
updateSurface(defaultURL)
else
updateSurface(self.getInputs()[1].value)
end
end
-- input function for the input box
function none() end function none() end
-- main function (can be called by other objects)
function updateSurface(newURL)
local obj_surface = getObjectFromGUID("721ba2")
local customInfo = obj_surface.getCustomObject()
if newURL ~= "" and newURL ~= nil and newURL ~= defaultURL then
customInfo.image = newURL
broadcastToAll("New Playmat Image Applied", { 0.2, 0.9, 0.2 })
else
customInfo.image = defaultURL
broadcastToAll("Default Playmat Image Applied", { 0.2, 0.9, 0.2 })
end
obj_surface.setCustomObject(customInfo)
obj_surface = obj_surface.reload()
end
-- creates the main button
function createOpenCloseButton()
if controlActive then
BUTTON_PARAMETERS.tooltip = "Close Playmat Panel"
else
BUTTON_PARAMETERS.tooltip = "Open Playmat Panel"
end
self.createButton(BUTTON_PARAMETERS)
end

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 cardIdIndex = { }
local classAndLevelIndex = { } local classAndLevelIndex = { }
local basicWeaknessList = { } local basicWeaknessList = { }
@ -114,15 +119,18 @@ function buildSupplementalIndexes()
for cardId, card in pairs(cardIdIndex) do for cardId, card in pairs(cardIdIndex) do
local cardData = card.data local cardData = card.data
local cardMetadata = card.metadata local cardMetadata = card.metadata
-- Add card to the basic weakness list, if appropriate. Some weaknesses have -- If the ID key and the metadata ID don't match this is a duplicate card created by an
-- multiple copies, and are added multiple times -- alternate_id, and we should skip it
if (cardMetadata.weakness and cardMetadata.basicWeaknessCount ~= nil) then if (cardId == cardMetadata.id) then
for i = 1, cardMetadata.basicWeaknessCount do -- Add card to the basic weakness list, if appropriate. Some weaknesses have
table.insert(basicWeaknessList, cardMetadata.id) -- multiple copies, and are added multiple times
if (cardMetadata.weakness and cardMetadata.basicWeaknessCount ~= nil) then
for i = 1, cardMetadata.basicWeaknessCount do
table.insert(basicWeaknessList, cardMetadata.id)
end end
end end
-- Add the card to the appropriate class and level indexes -- Add the card to the appropriate class and level indexes
local isGuardian = false local isGuardian = false
local isSeeker = false local isSeeker = false
local isMystic = false local isMystic = false
@ -130,35 +138,38 @@ function buildSupplementalIndexes()
local isSurvivor = false local isSurvivor = false
local isNeutral = false local isNeutral = false
local upgradeKey local upgradeKey
-- Excludes signature cards (which have no class or level) and alternate
-- ID entries
if (cardMetadata.class ~= nil and cardMetadata.level ~= nil) then if (cardMetadata.class ~= nil and cardMetadata.level ~= nil) then
isGuardian = string.match(cardMetadata.class, "Guardian") isGuardian = string.match(cardMetadata.class, "Guardian")
isSeeker = string.match(cardMetadata.class, "Seeker") isSeeker = string.match(cardMetadata.class, "Seeker")
isMystic = string.match(cardMetadata.class, "Mystic") isMystic = string.match(cardMetadata.class, "Mystic")
isRogue = string.match(cardMetadata.class, "Rogue") isRogue = string.match(cardMetadata.class, "Rogue")
isSurvivor = string.match(cardMetadata.class, "Survivor") isSurvivor = string.match(cardMetadata.class, "Survivor")
isNeutral = string.match(cardMetadata.class, "Neutral") isNeutral = string.match(cardMetadata.class, "Neutral")
if (cardMetadata.level > 0) then if (cardMetadata.level > 0) then
upgradeKey = "-upgrade" upgradeKey = "-upgrade"
else else
upgradeKey = "-level0" upgradeKey = "-level0"
end end
if (isGuardian) then if (isGuardian) then
table.insert(classAndLevelIndex["Guardian"..upgradeKey], cardMetadata.id) table.insert(classAndLevelIndex["Guardian"..upgradeKey], cardMetadata.id)
end end
if (isSeeker) then if (isSeeker) then
table.insert(classAndLevelIndex["Seeker"..upgradeKey], cardMetadata.id) table.insert(classAndLevelIndex["Seeker"..upgradeKey], cardMetadata.id)
end end
if (isMystic) then if (isMystic) then
table.insert(classAndLevelIndex["Mystic"..upgradeKey], cardMetadata.id) table.insert(classAndLevelIndex["Mystic"..upgradeKey], cardMetadata.id)
end end
if (isRogue) then if (isRogue) then
table.insert(classAndLevelIndex["Rogue"..upgradeKey], cardMetadata.id) table.insert(classAndLevelIndex["Rogue"..upgradeKey], cardMetadata.id)
end end
if (isSurvivor) then if (isSurvivor) then
table.insert(classAndLevelIndex["Survivor"..upgradeKey], cardMetadata.id) table.insert(classAndLevelIndex["Survivor"..upgradeKey], cardMetadata.id)
end end
if (isNeutral) then if (isNeutral) then
table.insert(classAndLevelIndex["Neutral"..upgradeKey], cardMetadata.id) table.insert(classAndLevelIndex["Neutral"..upgradeKey], cardMetadata.id)
end
end end
end end
end end
@ -172,6 +183,7 @@ end
function cardComparator(id1, id2) function cardComparator(id1, id2)
local card1 = cardIdIndex[id1] local card1 = cardIdIndex[id1]
local card2 = cardIdIndex[id2] local card2 = cardIdIndex[id2]
if (card1.metadata.level ~= card2.metadata.level) then if (card1.metadata.level ~= card2.metadata.level) then
return card1.metadata.level < card2.metadata.level return card1.metadata.level < card2.metadata.level
end end
@ -222,19 +234,92 @@ function getCardsByClassAndLevel(params)
return classAndLevelIndex[params.class..upgradeKey]; return classAndLevelIndex[params.class..upgradeKey];
end 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 -- 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 -- 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 -- occurs or the indexes are rebuilt, which will refresh the list to include all
-- weaknesses. -- weaknesses.
-- Return: String ID of the selected weakness. -- Return: String ID of the selected weakness.
function getRandomWeaknessId() function getRandomWeaknessId()
local pickedIndex = math.random(#basicWeaknessList) local availableWeaknesses = buildAvailableWeaknesses()
local weaknessId = basicWeaknessList[pickedIndex] if (#availableWeaknesses > 0) then
if (#basicWeaknessList > 1) then return availableWeaknesses[math.random(#availableWeaknesses)]
table.remove(basicWeaknessList, pickedIndex) end
else end
broadcastToAll("All weaknesses have been drawn!", {0.9, 0.2, 0.2})
-- 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 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 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() function buttonClick_draw()
local allCardsBag = getObjectFromGUID(allCardsBagGuid) local allCardsBag = getObjectFromGUID(allCardsBagGuid)
local weaknessId = allCardsBag.call("getRandomWeaknessId") 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 }) local card = allCardsBag.call("getCardById", { id = weaknessId })
spawnObjectData({ spawnObjectData({
data = card.data, data = card.data,

View File

@ -1,34 +1,4 @@
-- set true to enable debug logging local activeInvestigatorId = nil
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}
function log(message) function log(message)
if DEBUG then if DEBUG then
@ -66,6 +36,7 @@ function makeDiscardButton(position, searchPosition, discardPosition, number)
end end
function onload(save_state) function onload(save_state)
self.interactable = DEBUG self.interactable = DEBUG
DATA_HELPER = getObjectFromGUID('708279') DATA_HELPER = getObjectFromGUID('708279')
PLAYER_CARDS = DATA_HELPER.getTable('PLAYER_CARD_DATA') PLAYER_CARDS = DATA_HELPER.getTable('PLAYER_CARD_DATA')
@ -73,15 +44,16 @@ function onload(save_state)
-- positions of encounter card slots -- positions of encounter card slots
local encounterSlots = { local encounterSlots = {
{1, 0, -0.7}, {1.365, 0, -0.7},
{0.55, 0, -0.7}, {0.91, 0, -0.7},
{0.1, 0, -0.7}, {0.455, 0, -0.7},
{-0.35, 0, -0.7}, {0, 0, -0.7},
{-0.8, 0, -0.7} {-0.455, 0, -0.7},
{-0.91, 0, -0.7},
} }
local i = 1 local i = 1
while i <= 5 do while i <= 6 do
makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], encounterSlots[i], DISCARD_POSITION, i) makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], encounterSlots[i], DISCARD_POSITION, i)
i = i + 1 i = i + 1
end end
@ -90,7 +62,7 @@ function onload(save_state)
label = " ", label = " ",
click_function = "drawEncountercard", click_function = "drawEncountercard",
function_owner = self, function_owner = self,
position = {-1.45,0,-0.7}, position = {-1.88,0,-0.7},
rotation = {0,-15,0}, rotation = {0,-15,0},
width = 170, width = 170,
height = 255, height = 255,
@ -101,7 +73,7 @@ function onload(save_state)
label=" ", label=" ",
click_function = "drawChaostokenButton", click_function = "drawChaostokenButton",
function_owner = self, function_owner = self,
position = {1.48,0.0,-0.74}, position = {1.84,0.0,-0.74},
rotation = {0,-45,0}, rotation = {0,-45,0},
width = 125, width = 125,
height = 125, height = 125,
@ -112,24 +84,13 @@ function onload(save_state)
label="Upkeep", label="Upkeep",
click_function = "doUpkeep", click_function = "doUpkeep",
function_owner = self, function_owner = self,
position = {1.48,0.1,-0.44}, position = {1.84,0.1,-0.44},
scale = {0.12, 0.12, 0.12}, scale = {0.12, 0.12, 0.12},
width = 800, width = 800,
height = 280, height = 280,
font_size = 180 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) local state = JSON.decode(save_state)
if state ~= nil then if state ~= nil then
if state.playerColor ~= nil then if state.playerColor ~= nil then
@ -157,34 +118,31 @@ function setMessageColor(color)
messageColor = Player[PLAYER_COLOR].seated and PLAYER_COLOR or color messageColor = Player[PLAYER_COLOR].seated and PLAYER_COLOR or color
end end
function getDrawDiscardDecks(zone) -- get the draw deck and discard pile objects
-- get the draw deck and discard pile objects function getDrawDiscardDecks()
drawDeck = nil drawDeck = nil
discardPile = nil discardPile = nil
topCard = nil
local zone = getObjectFromGUID(zoneID)
if zone == nil then return end
for i,object in ipairs(zone.getObjects()) do for i,object in ipairs(zone.getObjects()) do
if object.tag == "Deck" or object.tag == "Card" then if object.tag == "Deck" or object.tag == "Card" then
if object.is_face_down then local relativePos = self.positionToLocal(object.getPosition())
drawDeck = object if relativePos.z > 0.5 then
else
discardPile = object discardPile = object
else
if investigator == "Norman Withers" and object.tag == "Card" and not object.is_face_down then
topCard = object
else
drawDeck = object
end
end end
end end
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) function doUpkeep(obj, color, alt_click)
-- right-click binds to new player color -- right-click binds to new player color
if alt_click then if alt_click then
@ -206,70 +164,93 @@ function doUpkeep(obj, color, alt_click)
local y = PLAY_ZONE_ROTATION.y local y = PLAY_ZONE_ROTATION.y
local investigator = nil investigator = nil
local miniId = nil
local forcedLearning = false
for i,v in ipairs(objs) do for i,v in ipairs(objs) do
local obj = v.hit_object 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 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() 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 if string.match(name, "Jenny Barnes") ~= nil then
investigator = "Jenny Barnes" investigator = "Jenny Barnes"
elseif name == "Patrice Hathaway" then elseif name == "Patrice Hathaway" then
investigator = name investigator = name
elseif string.match(name, "Norman Withers") ~= nil then
investigator = "Norman Withers"
end end
elseif name == "Forced Learning" then
forcedLearning = true
else else
local r = obj.getRotation() local r = obj.getRotation()
if (r.y - y > 10) or (y - r.y > 10) then if (r.y - y > 10) or (y - r.y > 10) then
obj.setRotation(PLAY_ZONE_ROTATION) obj.setRotation(PLAY_ZONE_ROTATION)
end end
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 if obj.is_face_down then obj.flip() end
end 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 -- gain resource
getObjectFromGUID(RESOURCE_COUNTER_GUID).call("add_subtract") getObjectFromGUID(RESOURCE_COUNTER_GUID).call("add_subtract")
if investigator == "Jenny Barnes" then if investigator == "Jenny Barnes" then
getObjectFromGUID(RESOURCE_COUNTER_GUID).call("add_subtract") getObjectFromGUID(RESOURCE_COUNTER_GUID).call("add_subtract")
printToColor("Taking 2 resources (Jenny)", messageColor) printToColor("Gaining 2 resources (Jenny)", messageColor)
end 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) -- special draw for Patrice Hathaway (shuffle discards if necessary)
if investigator == "Patrice Hathaway" then if investigator == "Patrice Hathaway" then
patriceDraw() patriceDraw()
return return
end end
-- draw 1 card (shuffle discards if necessary) -- special draw for Forced Learning
checkDeckThenDrawOne() if forcedLearning then
end forcedLearningDraw()
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 return
end end
drawCardsWithReshuffle(1)
end
function doDrawOne(obj, color)
setMessageColor(color) setMessageColor(color)
drawCardsWithReshuffle(1)
-- 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 end
function doNotReady(card) function doNotReady(card)
@ -280,13 +261,67 @@ function doNotReady(card)
end end
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) function drawCards(numCards)
if drawDeck == nil then return end if drawDeck == nil then return end
drawDeck.deal(numCards, PLAYER_COLOR) drawDeck.deal(numCards, PLAYER_COLOR)
end end
function shuffleDiscardIntoDeck() function shuffleDiscardIntoDeck()
discardPile.flip() if not discardPile.is_face_down then discardPile.flip() end
discardPile.shuffle() discardPile.shuffle()
discardPile.setPositionSmooth(DRAW_DECK_POSITION, false, false) discardPile.setPositionSmooth(DRAW_DECK_POSITION, false, false)
drawDeck = discardPile drawDeck = discardPile
@ -297,27 +332,13 @@ function patriceDraw()
local handSize = #Player[PLAYER_COLOR].getHandObjects() local handSize = #Player[PLAYER_COLOR].getHandObjects()
if handSize >= 5 then return end if handSize >= 5 then return end
local cardsToDraw = 5 - handSize local cardsToDraw = 5 - handSize
local deckSize
printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor) printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor)
if drawDeck == nil then drawCardsWithReshuffle(cardsToDraw)
deckSize = 0 end
elseif drawDeck.tag == "Deck" then
deckSize = #drawDeck.getObjects()
else
deckSize = 1
end
if deckSize >= cardsToDraw then function forcedLearningDraw()
drawCards(cardsToDraw) printToColor("Drawing 2 cards, discard 1 (Forced Learning)", messageColor)
return drawCardsWithReshuffle(2)
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 end
function checkDeckZoneExists() function checkDeckZoneExists()
@ -368,6 +389,9 @@ end
-- spawn a group of tokens of the given type on the object -- spawn a group of tokens of the given type on the object
function spawnTokenGroup(object, tokenType, tokenCount) function spawnTokenGroup(object, tokenType, tokenCount)
if (tokenCount < 1 or tokenCount > 12) then
return
end
local offsets = PLAYER_CARD_TOKEN_OFFSETS[tokenCount] local offsets = PLAYER_CARD_TOKEN_OFFSETS[tokenCount]
if offsets == nil then if offsets == nil then
error("couldn't find offsets for " .. tokenCount .. ' tokens') 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 -- we assume we shouldn't spawn tokens if in doubt, this should
-- only ever happen on load and in that case prevents respawns -- only ever happen on load and in that case prevents respawns
local spawned = DATA_HELPER.call('getSpawnedPlayerCardGuid', {object.getGUID()}) local spawned = DATA_HELPER.call('getSpawnedPlayerCardGuid', {object.getGUID()})
local canSpawn = getPlayerCardData(object) local hasDataHelperData = getPlayerCardData(object)
return not spawned and canSpawn local cardMetadata = JSON.decode(object.getGMNotes()) or {}
local hasUses = cardMetadata.uses ~= nil
return not spawned and (hasDataHelperData or hasUses)
end end
function markSpawned(object) function markSpawned(object)
@ -404,12 +430,32 @@ function markSpawned(object)
end end
function spawnTokensFor(object) function spawnTokensFor(object)
local data = getPlayerCardData(object) local cardMetadata = JSON.decode(object.getGMNotes()) or {}
if data == nil then local token = nil
error('attempt to spawn tokens for ' .. object.getName() .. ': no token data') local type = nil
local tokenCount = 0
if (cardMetadata.uses ~= nil) then
for i, useInfo in ipairs(cardMetadata.uses) do
token = useInfo.token
type = useInfo.type
tokenCount = useInfo.count
if (activeInvestigatorId == "03004" and useInfo.type == "Charge") then
tokenCount = tokenCount + 1
end
log("Spawning tokens for "..object.getName()..'['..object.getDescription()..']: '..tokenCount.."x "..token)
spawnTokenGroup(object, token, tokenCount)
end
else
local data = getPlayerCardData(object)
if data == nil then
error('attempt to spawn tokens for ' .. object.getName() .. ': no token data')
end
token = data['tokenType']
tokenCount = data['tokenCount']
log(object.getName() .. '[' .. object.getDescription() .. ']' .. ' : ' .. data['tokenType'] .. ' : ' .. data['tokenCount'])
log("Spawning tokens for "..object.getName()..'['..object.getDescription()..']: '..tokenCount.."x "..token)
spawnTokenGroup(object, token, tokenCount)
end end
log(object.getName() .. '[' .. object.getDescription() .. ']' .. ' : ' .. data['tokenType'] .. ' : ' .. data['tokenCount'])
spawnTokenGroup(object, data['tokenType'], data['tokenCount'])
markSpawned(object) markSpawned(object)
end end
@ -438,6 +484,9 @@ function unmarkSpawned(guid, force)
end end
function onCollisionEnter(collision_info) function onCollisionEnter(collision_info)
if (collision_info.collision_object.name == "Card") then
maybeUpdateActiveInvestigator(collision_info.collision_object)
end
if not COLLISION_ENABLED then if not COLLISION_ENABLED then
return return
end end
@ -456,6 +505,28 @@ function onCollisionEnter(collision_info)
end end
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 -- functions delegated to Global
function drawChaostokenButton(object, player, isRightClick) function drawChaostokenButton(object, player, isRightClick)
-- local toPosition = self.positionToWorld(DRAWN_CHAOS_TOKEN_OFFSET) -- 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 } BLESS_COLOR = { r = 0.3, g = 0.25, b = 0.09 }
CURSE_COLOR = { r=0.2, g=0.08, b=0.24 } CURSE_COLOR = { r = 0.2, g = 0.08, b = 0.24 }
MIN_VALUE = 1 MIN_VALUE = 1
MAX_VALUE = 10 MAX_VALUE = 10
IMAGE_URL = { IMAGE_URL = {
@ -9,120 +9,122 @@ IMAGE_URL = {
function onload() function onload()
self.createButton({ self.createButton({
label="Add", label = "Add",
click_function="addBlessToken", click_function = "addBlessToken",
function_owner=self, function_owner = self,
position={-2.3,0.1,-0.5}, position = { -2.3, 0.1, -0.5 },
height=150, height = 150,
width=300, width = 300,
scale={x=1.75, y=1.75, z=1.75}, scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size=100, font_size = 100,
font_color={ r=1, g=1, b=1 }, font_color = { r = 1, g = 1, b = 1 },
color=BLESS_COLOR color = BLESS_COLOR
}) })
self.createButton({ self.createButton({
label="Remove", label = "Remove",
click_function="removeBlessToken", click_function = "removeBlessToken",
function_owner=self, function_owner = self,
position={-0.9,0.1,-0.5}, position = { -0.9, 0.1, -0.5 },
height=150, height = 150,
width=450, width = 450,
scale={x=1.75, y=1.75, z=1.75}, scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size=100, font_size = 100,
font_color={ r=1, g=1, b=1 }, font_color = { r = 1, g = 1, b = 1 },
color=BLESS_COLOR color = BLESS_COLOR
}) })
self.createButton({ self.createButton({
label="Take", label = "Take",
click_function="takeBlessToken", click_function = "takeBlessToken",
function_owner=self, function_owner = self,
position={0.7,0.1,-0.5}, position = { 0.7, 0.1, -0.5 },
height=150, height = 150,
width=350, width = 350,
scale={x=1.75, y=1.75, z=1.75}, scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size=100, font_size = 100,
font_color={ r=1, g=1, b=1 }, font_color = { r = 1, g = 1, b = 1 },
color=BLESS_COLOR color = BLESS_COLOR
}) })
self.createButton({ self.createButton({
label="Return", label = "Return",
click_function="returnBlessToken", click_function = "returnBlessToken",
function_owner=self, function_owner = self,
position={2.1,0.1,-0.5}, position = { 2.1, 0.1, -0.5 },
height=150, height = 150,
width=400, width = 400,
scale={x=1.75, y=1.75, z=1.75}, scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size=100, font_size = 100,
font_color={ r=1, g=1, b=1 }, font_color = { r = 1, g = 1, b = 1 },
color=BLESS_COLOR color = BLESS_COLOR
}) })
self.createButton({ self.createButton({
label="Add", label = "Add",
click_function="addCurseToken", click_function = "addCurseToken",
function_owner=self, function_owner = self,
position={-2.3,0.1,0.5}, position = { -2.3, 0.1, 0.5 },
height=150, height = 150,
width=300, width = 300,
scale={x=1.75, y=1.75, z=1.75}, scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size=100, font_size = 100,
font_color={ r=1, g=1, b=1 }, font_color = { r = 1, g = 1, b = 1 },
color=CURSE_COLOR color = CURSE_COLOR
}) })
self.createButton({ self.createButton({
label="Remove", label = "Remove",
click_function="removeCurseToken", click_function = "removeCurseToken",
function_owner=self, function_owner = self,
position={-0.9,0.1,0.5}, position = { -0.9, 0.1, 0.5 },
height=150, height = 150,
width=450, width = 450,
scale={x=1.75, y=1.75, z=1.75}, scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size=100, font_size = 100,
font_color={ r=1, g=1, b=1 }, font_color = { r = 1, g = 1, b = 1 },
color=CURSE_COLOR color = CURSE_COLOR
}) })
self.createButton({ self.createButton({
label="Take", label = "Take",
click_function="takeCurseToken", click_function = "takeCurseToken",
function_owner=self, function_owner = self,
position={0.7,0.1,0.5}, position = { 0.7, 0.1, 0.5 },
height=150, height = 150,
width=350, width = 350,
scale={x=1.75, y=1.75, z=1.75}, scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size=100, font_size = 100,
font_color={ r=1, g=1, b=1 }, font_color = { r = 1, g = 1, b = 1 },
color=CURSE_COLOR color = CURSE_COLOR
}) })
self.createButton({ self.createButton({
label="Return", label = "Return",
click_function="returnCurseToken", click_function = "returnCurseToken",
function_owner=self, function_owner = self,
position={2.1,0.1,0.5}, position = { 2.1, 0.1, 0.5 },
height=150, height = 150,
width=400, width = 400,
scale={x=1.75, y=1.75, z=1.75}, scale = { x = 1.75, y = 1.75, z = 1.75 },
font_size=100, font_size = 100,
font_color={ r=1, g=1, b=1 }, font_color = { r = 1, g = 1, b = 1 },
color=CURSE_COLOR color = CURSE_COLOR
}) })
self.createButton({ self.createButton({
label="Reset", click_function="doReset", function_owner=self, label = "Reset", click_function = "doReset", function_owner = self,
position={0,0.3,1.8}, rotation={0,0,0}, height=350, width=800, position = { 0, 0, 1.8 }, rotation = { 0, 0, 0 }, height = 350, width = 800,
font_size=250, color={0,0,0}, font_color={1,1,1} font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
}) })
numInPlay = { Bless=0, Curse=0 } numInPlay = { Bless = 0, Curse = 0 }
tokensTaken = { Bless={}, Curse={} } tokensTaken = { Bless = {}, Curse = {} }
sealedTokens = {}
Wait.time(initializeState, 1) Wait.time(initializeState, 1)
addHotkey("Bless Curse Status", printStatus, false) addHotkey("Bless Curse Status", printStatus, false)
addHotkey("Wendy's Menu", addMenuOptions, false)
end end
function initializeState() function initializeState()
@ -131,7 +133,7 @@ function initializeState()
local chaosbag = getChaosBag() local chaosbag = getChaosBag()
if chaosbag == nil then return end if chaosbag == nil then return end
local tokens = {} local tokens = {}
for i,v in ipairs(chaosbag.getObjects()) do for i, v in ipairs(chaosbag.getObjects()) do
if v.name == "Bless" then if v.name == "Bless" then
numInPlay.Bless = numInPlay.Bless + 1 numInPlay.Bless = numInPlay.Bless + 1
elseif v.name == "Curse" then elseif v.name == "Curse" then
@ -140,22 +142,17 @@ function initializeState()
end end
-- find tokens in the play area -- find tokens in the play area
local objs = Physics.cast({ local objs = getObjects()
origin = { x=-33, y=0, z=0.5 }, for i, obj in ipairs(objs) do
direction = { x=0, y=1, z=0 }, local pos = obj.getPosition()
type = 3, if (pos.x > -110 and pos.x < 44 and pos.z > -77 and pos.z < 77) then
size = { x=77, y=5, z=77 }, if obj.getName() == "Bless" then
orientation = { x=0, y=90, z=0 } table.insert(tokensTaken.Bless, obj.getGUID())
}) numInPlay.Bless = numInPlay.Bless + 1
elseif obj.getName() == "Curse" then
for i,v in ipairs(objs) do table.insert(tokensTaken.Curse, obj.getGUID())
local obj = v.hit_object numInPlay.Curse = numInPlay.Curse + 1
if obj.getName() == "Bless" then end
table.insert(tokensTaken.Bless, obj.getGUID())
numInPlay.Bless = numInPlay.Bless + 1
elseif obj.getName() == "Curse" then
table.insert(tokensTaken.Curse, obj.getGUID())
numInPlay.Curse = numInPlay.Curse + 1
end end
end end
@ -174,8 +171,8 @@ end
function doReset(_obj, _color, alt_click) function doReset(_obj, _color, alt_click)
playerColor = _color playerColor = _color
numInPlay = { Bless=0, Curse=0 } numInPlay = { Bless = 0, Curse = 0 }
tokensTaken = { Bless={}, Curse={} } tokensTaken = { Bless = {}, Curse = {} }
initializeState() initializeState()
end end
@ -202,8 +199,8 @@ function spawnToken()
local url = IMAGE_URL[mode] local url = IMAGE_URL[mode]
local obj = spawnObject({ local obj = spawnObject({
type = 'Custom_Tile', type = 'Custom_Tile',
position = {pos.x, pos.y + 3, pos.z}, position = { pos.x, pos.y + 3, pos.z },
rotation = {x = 0, y = 260, z = 0}, rotation = { x = 0, y = 260, z = 0 },
callback_function = spawn_callback callback_function = spawn_callback
}) })
obj.setCustomObject({ obj.setCustomObject({
@ -211,7 +208,7 @@ function spawnToken()
image = url, image = url,
thickness = 0.10, thickness = 0.10,
}) })
obj.scale {0.81, 1, 0.81} obj.scale { 0.81, 1, 0.81 }
return obj return obj
end end
@ -242,8 +239,14 @@ function takeToken(type, _color, remove)
playerColor = _color playerColor = _color
local chaosbag = getChaosBag() local chaosbag = getChaosBag()
if chaosbag == nil then return end 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 = {} local tokens = {}
for i,v in ipairs(chaosbag.getObjects()) do for i, v in ipairs(chaosbag.getObjects()) do
if v.name == type then if v.name == type then
table.insert(tokens, v.guid) table.insert(tokens, v.guid)
end end
@ -260,9 +263,13 @@ function takeToken(type, _color, remove)
end end
local guid = table.remove(tokens) local guid = table.remove(tokens)
mode = type 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({ chaosbag.takeObject({
guid = guid, guid = guid,
position = {pos.x-2, pos.y, pos.z}, position = position,
smooth = false, smooth = false,
callback_function = callback callback_function = callback
}) })
@ -314,7 +321,7 @@ end
function getChaosBag() function getChaosBag()
local items = getObjectFromGUID("83ef06").getObjects() local items = getObjectFromGUID("83ef06").getObjects()
local chaosbag = nil local chaosbag = nil
for i,v in ipairs(items) do for i, v in ipairs(items) do
if v.getDescription() == "Chaos Bag" then if v.getDescription() == "Chaos Bag" then
chaosbag = getObjectFromGUID(v.getGUID()) chaosbag = getObjectFromGUID(v.getGUID())
break break
@ -334,3 +341,104 @@ function getTokenCount()
return "(" .. (numInPlay[mode] - #tokensTaken[mode]) .. "/" .. return "(" .. (numInPlay[mode] - #tokensTaken[mode]) .. "/" ..
#tokensTaken[mode] .. ")" #tokensTaken[mode] .. ")"
end 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() function updateSave()
local data_to_save = {["ml"]=memoryList} local data_to_save = { ["ml"] = memoryList }
saved_data = JSON.encode(data_to_save) saved_data = JSON.encode(data_to_save)
self.script_state = saved_data self.script_state = saved_data
end end
@ -21,16 +21,20 @@ function onload(saved_data)
end end
end end
--Beginning Setup --Beginning Setup
--Make setup button --Make setup button
function createSetupButton() function createSetupButton()
self.createButton({ self.createButton({
label="Setup", click_function="buttonClick_setup", function_owner=self, label = "Setup",
position={0,5,-2}, rotation={0,0,0}, height=250, width=600, click_function = "buttonClick_setup",
font_size=150, color={0,0,0}, font_color={1,1,1} 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 end
@ -50,7 +54,7 @@ function createButtonsOnAllObjects()
if obj ~= self then if obj ~= self then
local dummyIndex = howManyButtons local dummyIndex = howManyButtons
--On a normal bag, the button positions aren't the same size as the bag. --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 --Super sweet math to set button positions
local selfPos = self.getPosition() local selfPos = self.getPosition()
local objPos = obj.getPosition() local objPos = obj.getPosition()
@ -67,9 +71,9 @@ function createButtonsOnAllObjects()
local func = function() buttonClick_selection(dummyIndex, obj) end local func = function() buttonClick_selection(dummyIndex, obj) end
self.setVar(funcName, func) self.setVar(funcName, func)
self.createButton({ self.createButton({
click_function=funcName, function_owner=self, click_function = funcName, function_owner = self,
position=objPos, rotation=rot, height=1000, width=1000, position = objPos, rotation = rot, height = 1000, width = 1000,
color={0.75,0.25,0.25,0.6}, color = { 0.75, 0.25, 0.25, 0.6 },
}) })
howManyButtons = howManyButtons + 1 howManyButtons = howManyButtons + 1
end end
@ -79,43 +83,40 @@ end
--Creates submit and cancel buttons --Creates submit and cancel buttons
function createSetupActionButtons() function createSetupActionButtons()
self.createButton({ self.createButton({
label="Cancel", click_function="buttonClick_cancel", function_owner=self, label = "Cancel", click_function = "buttonClick_cancel", function_owner = self,
position={1.5,5,2}, rotation={0,0,0}, height=350, width=1100, 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} font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
}) })
self.createButton({ self.createButton({
label="Submit", click_function="buttonClick_submit", function_owner=self, label = "Submit", click_function = "buttonClick_submit", function_owner = self,
position={-1.2,5,2}, rotation={0,0,0}, height=350, width=1100, 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} font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
}) })
self.createButton({ self.createButton({
label="Reset", click_function="buttonClick_reset", function_owner=self, label = "Reset", click_function = "buttonClick_reset", function_owner = self,
position={-3.5,5,2}, rotation={0,0,0}, height=350, width=800, 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} font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
}) })
end end
--During Setup --During Setup
--Checks or unchecks buttons --Checks or unchecks buttons
function buttonClick_selection(index, obj) 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 if memoryList[obj.getGUID()] == nil then
self.editButton({index=index, color=color}) self.editButton({ index = index, color = color })
--Adding pos/rot to memory table --Adding pos/rot to memory table
local pos, rot = obj.getPosition(), obj.getRotation() local pos, rot = obj.getPosition(), obj.getRotation()
--I need to add it like this or it won't save due to indexing issue --I need to add it like this or it won't save due to indexing issue
memoryList[obj.getGUID()] = { memoryList[obj.getGUID()] = {
pos={x=round(pos.x,4), y=round(pos.y,4), z=round(pos.z,4)}, 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)}, rot = { x = round(rot.x, 4), y = round(rot.y, 4), z = round(rot.z, 4) },
lock=obj.getLock() lock = obj.getLock()
} }
obj.highlightOn({0,1,0}) obj.highlightOn({ 0, 1, 0 })
else else
color = {0.75,0.25,0.25,0.6} color = { 0.75, 0.25, 0.25, 0.6 }
self.editButton({index=index, color=color}) self.editButton({ index = index, color = color })
memoryList[obj.getGUID()] = nil memoryList[obj.getGUID()] = nil
obj.highlightOff() obj.highlightOff()
end end
@ -131,13 +132,13 @@ function buttonClick_cancel()
createMemoryActionButtons() createMemoryActionButtons()
end end
removeAllHighlights() removeAllHighlights()
broadcastToAll("Selection Canceled", {1,1,1}) broadcastToAll("Selection Canceled", { 1, 1, 1 })
end end
--Saves selections --Saves selections
function buttonClick_submit() function buttonClick_submit()
if next(memoryList) == nil then 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 else
self.clearButtons() self.clearButtons()
createMemoryActionButtons() createMemoryActionButtons()
@ -147,7 +148,7 @@ function buttonClick_submit()
local obj = getObjectFromGUID(guid) local obj = getObjectFromGUID(guid)
if obj ~= nil then obj.highlightOff() end if obj ~= nil then obj.highlightOff() end
end end
broadcastToAll(count.." Objects Saved", {1,1,1}) broadcastToAll(count .. " Objects Saved", { 1, 1, 1 })
updateSave() updateSave()
end end
end end
@ -158,31 +159,55 @@ function buttonClick_reset()
self.clearButtons() self.clearButtons()
createSetupButton() createSetupButton()
removeAllHighlights() removeAllHighlights()
broadcastToAll("Tool Reset", {1,1,1}) broadcastToAll("Tool Reset", { 1, 1, 1 })
updateSave() updateSave()
end end
--After Setup --After Setup
--Creates recall and place buttons --Creates recall and place buttons
function createMemoryActionButtons() function createMemoryActionButtons()
self.createButton({ self.createButton({
label="Clicker", click_function="buttonClick_place", function_owner=self, label = "Clicker", click_function = "buttonClick_place", function_owner = self,
position={4.2,1,0}, rotation={0,0,0}, height=500, width=1100, 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} font_size = 350, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }
}) })
self.createButton({ self.createButton({
label="Counter", click_function="buttonClick_recall", function_owner=self, label = "Counter", click_function = "buttonClick_recall", function_owner = self,
position={-4.2,1,-0.1}, rotation={0,0,0}, height=500, width=1300, 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} font_size=350, color={0,0,0}, font_color={1,1,1}
}) })
-- self.createButton({ --]]
-- label="Setup", click_function="buttonClick_setup", function_owner=self, end
-- position={-6,1,0}, rotation={0,90,0}, height=500, width=1200,
-- font_size=350, color={0,0,0}, font_color={1,1,1} 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 end
--Sends objects from bag/table to their saved position/rotation --Sends objects from bag/table to their saved position/rotation
@ -200,7 +225,7 @@ function buttonClick_place()
for _, bagObj in ipairs(bagObjList) do for _, bagObj in ipairs(bagObjList) do
if bagObj.guid == guid then if bagObj.guid == guid then
local item = self.takeObject({ local item = self.takeObject({
guid=guid, position=entry.pos, rotation=entry.rot, guid = guid, position = entry.pos, rotation = entry.rot,
}) })
item.setLock(entry.lock) item.setLock(entry.lock)
break break
@ -208,7 +233,7 @@ function buttonClick_place()
end end
end end
end end
broadcastToAll("Objects Placed", {1,1,1}) broadcastToAll("Objects Placed", { 1, 1, 1 })
end end
--Recalls objects to bag from table --Recalls objects to bag from table
@ -217,31 +242,27 @@ function buttonClick_recall()
local obj = getObjectFromGUID(guid) local obj = getObjectFromGUID(guid)
if obj ~= nil then self.putObject(obj) end if obj ~= nil then self.putObject(obj) end
end end
broadcastToAll("Objects Recalled", {1,1,1}) broadcastToAll("Objects Recalled", { 1, 1, 1 })
end end
--Utility functions --Utility functions
--Find delta (difference) between 2 x/y/z coordinates --Find delta (difference) between 2 x/y/z coordinates
function findOffsetDistance(p1, p2, obj) function findOffsetDistance(p1, p2, obj)
local deltaPos = {} local deltaPos = {}
local bounds = obj.getBounds() local bounds = obj.getBounds()
deltaPos.x = (p2.x-p1.x) deltaPos.x = (p2.x - p1.x)
deltaPos.y = (p2.y-p1.y) + (bounds.size.y - bounds.offset.y) deltaPos.y = (p2.y - p1.y) + (bounds.size.y - bounds.offset.y)
deltaPos.z = (p2.z-p1.z) deltaPos.z = (p2.z - p1.z)
return deltaPos return deltaPos
end end
--Used to rotate a set of coordinates by an angle --Used to rotate a set of coordinates by an angle
function rotateLocalCoordinates(desiredPos, obj) function rotateLocalCoordinates(desiredPos, obj)
local objPos, objRot = obj.getPosition(), obj.getRotation() local objPos, objRot = obj.getPosition(), obj.getRotation()
local angle = math.rad(objRot.y) local angle = math.rad(objRot.y)
local x = desiredPos.x * math.cos(angle) - desiredPos.z * math.sin(angle) local x = desiredPos.x * math.cos(angle) - desiredPos.z * math.sin(angle)
local z = desiredPos.x * math.sin(angle) + desiredPos.z * math.cos(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 end
--Coroutine delay, in seconds --Coroutine delay, in seconds
@ -268,6 +289,6 @@ end
--Round number (num) to the Nth decimal (dec) --Round number (num) to the Nth decimal (dec)
function round(num, 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 return math.floor(num * mult + 0.5) / mult
end 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>