Initial code extraction and commit
Initialize the SCED repository by moving most scripts from TTS objects to separate files which are included via require().
This commit is contained in:
parent
852e384532
commit
83f75c2318
287
src/arkhamdb/CommandManager.ttslua
Normal file
287
src/arkhamdb/CommandManager.ttslua
Normal file
@ -0,0 +1,287 @@
|
||||
---
|
||||
--- 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
|
165
src/arkhamdb/Configuration.ttslua
Normal file
165
src/arkhamdb/Configuration.ttslua
Normal file
@ -0,0 +1,165 @@
|
||||
---
|
||||
--- Generated by EmmyLua(https://github.com/EmmyLua)
|
||||
--- Created by Whimsical.
|
||||
--- DateTime: 2021-08-19 6:39 a.m.
|
||||
---
|
||||
|
||||
local Priority = {
|
||||
ERROR = 0,
|
||||
WARNING = 1,
|
||||
INFO = 2,
|
||||
DEBUG = 3
|
||||
}
|
||||
|
||||
---@type ArkhamImportConfiguration
|
||||
configuration = {
|
||||
api_uri = "https://arkhamdb.com/api/public",
|
||||
priority = Priority.INFO,
|
||||
public_deck = "decklist",
|
||||
private_deck = "deck",
|
||||
cards = "card",
|
||||
taboo = "taboos",
|
||||
card_bag_guid = "15bb07",
|
||||
weaknesses_bag_guid = "770c4e",
|
||||
investigator_bag_guid = "84a000",
|
||||
minicard_bag_guid = "80b12d",
|
||||
ui_builder_guid = "ddd2eb",
|
||||
command_manager_guid = "a0b1de",
|
||||
bonded_cards = {
|
||||
["05313"] = { -- Hallowed Mirror / Soothing Melody
|
||||
code = "05314",
|
||||
count = 3
|
||||
},
|
||||
["54002"] = { -- Hallowed Mirror (3) / Soothing Melody
|
||||
code = "05314",
|
||||
count = 3
|
||||
},
|
||||
["05316"] = { -- Occult Lexicon / Blood-Rite
|
||||
code = "05317",
|
||||
count = 3
|
||||
},
|
||||
["54004"] = { -- Occult Lexicon (3) / Blood-Rite
|
||||
code = "05317",
|
||||
count = 3
|
||||
},
|
||||
["06018"] = { -- The Hungering Blade / Bloodlust
|
||||
code = "06019",
|
||||
count = 3
|
||||
},
|
||||
["06330"] = { -- Nightmare Bauble / Dream Parasite
|
||||
code = "06331",
|
||||
count = 3
|
||||
},
|
||||
["06030"] = { -- Miss Doyle / Hope/ Zeal / Augur
|
||||
{
|
||||
code = "06031",
|
||||
count = 1
|
||||
},
|
||||
{
|
||||
code = "06032",
|
||||
count = 1
|
||||
},
|
||||
{
|
||||
code = "06033",
|
||||
count = 1
|
||||
}
|
||||
},
|
||||
["06013"] = { -- Gate Box / Dream-Gate
|
||||
code = "06015a",
|
||||
count = 1
|
||||
},
|
||||
["06024"] = { -- Crystallizer of Dreams / Guardian of the Crystallizer
|
||||
code = "06025",
|
||||
count = 2
|
||||
},
|
||||
["06112"] = { -- Dream Diary / Essence of the Dream
|
||||
code = "06113",
|
||||
count = 1
|
||||
},
|
||||
["06236"] = { -- Dream Diary / Essence of the Dream
|
||||
code = "06113",
|
||||
count = 1
|
||||
},
|
||||
["06237"] = { -- Dream Diary / Essence of the Dream
|
||||
code = "06113",
|
||||
count = 1
|
||||
},
|
||||
["06238"] = { -- Dream Diary / Essence of the Dream
|
||||
code = "06113",
|
||||
count = 1
|
||||
},
|
||||
["06276"] = { -- Empty Vessel / Wish Eater
|
||||
code = "06277",
|
||||
count = 1
|
||||
},
|
||||
["06021"] = { -- Segment of Onyx / Pendant of the Queen
|
||||
code = "06022",
|
||||
count = 1
|
||||
},
|
||||
["06027"] = { -- Stargazing / The Stars Are Right
|
||||
code = "06028",
|
||||
count = 2
|
||||
},
|
||||
["06282"] = { -- Summoned Hound / Unbound Beast
|
||||
code = "06283",
|
||||
count = 2
|
||||
}
|
||||
},
|
||||
default_zone_overrides = {
|
||||
["02014"] = "bonded", -- Duke
|
||||
["03009"] = "bonded", -- Sophie
|
||||
["06013"] = "bonded", -- Gate Box
|
||||
["05014"] = "bonded" -- Dark Insight
|
||||
},
|
||||
discriminators = {
|
||||
["53010"] = "Permanent", -- On Your Own (Permanent)
|
||||
["01008"] = "Signature", -- Daisy's Tote Bag
|
||||
["90002"] = "Advanced", -- Daisy's Tote Bag Advanced
|
||||
["90003"] = "John Dee Translation (Advanced)", -- The Necronomicon
|
||||
["01010"] = "Signature", -- On the Lam
|
||||
["90009"] = "Advanced", -- On the Lam
|
||||
["01011"] = "Signature", -- Hospital Debts
|
||||
["90010"] = "Advanced", -- Hospital Debts
|
||||
["01013"] = "Signature", -- Dark Memory
|
||||
["90019"] = "Advanced", -- Dark Memory
|
||||
["90018"] = "Artifact from Another Life (Advanced)", -- Heirloom of Hyperborea
|
||||
["90030"] = "Advanced", -- Roland's .38 Special
|
||||
["01006"] = "Signature", -- Roland's .38 Special
|
||||
["90031"] = "Advanced", -- Cover Up
|
||||
["01007"] = "Signature", -- Cover Up
|
||||
["05186"] = "Guardian", -- .45 Thompson
|
||||
["05187"] = "Rogue", -- .45 Thompson
|
||||
["05188"] = "Seeker", -- Scroll of Secrets
|
||||
["05189"] = "Mystic", -- Scroll of Secrets
|
||||
["05190"] = "Rogue", -- Tennessee Sour Mash
|
||||
["05191"] = "Survivor", -- Tennessee Sour Mash
|
||||
["05192"] = "Guardian", -- Enchanted Blade
|
||||
["05193"] = "Mystic", -- Enchanted Blade
|
||||
["05194"] = "Seeker", -- Grisly Totem
|
||||
["05195"] = "Survivor", -- Grisly Totem
|
||||
["06015a"] = "", -- Dream-Gate
|
||||
},
|
||||
zones = {
|
||||
default = {
|
||||
position = Vector(0.285, 1.5, 0.40),
|
||||
is_facedown = true
|
||||
},
|
||||
permanent = {
|
||||
position = Vector(-0.285, 1.5, 0.40),
|
||||
is_facedown = false
|
||||
},
|
||||
weakness = {
|
||||
position = Vector(-0.855, 1.5, 0.40),
|
||||
is_facedown = true
|
||||
},
|
||||
bonded = {
|
||||
position = Vector(-1.425, 1.5, 0.40),
|
||||
is_facedown = false
|
||||
},
|
||||
investigator = {
|
||||
position = Vector(1.150711, 1.5, 0.454833),
|
||||
is_facedown = false
|
||||
}
|
||||
},
|
||||
debug_deck_id = nil,
|
||||
}
|
718
src/arkhamdb/DeckImporterMain.ttslua
Normal file
718
src/arkhamdb/DeckImporterMain.ttslua
Normal file
@ -0,0 +1,718 @@
|
||||
---
|
||||
--- Generated by EmmyLua(https://github.com/EmmyLua)
|
||||
--- Created by Whimsical.
|
||||
--- DateTime: 2021-08-19 6:38 a.m.
|
||||
---
|
||||
|
||||
---@type ArkhamImportConfiguration
|
||||
|
||||
require("src/arkhamdb/LoaderUi")
|
||||
local Zones = require("src/arkhamdb/Zones")
|
||||
|
||||
local RANDOM_WEAKNESS_ID = "01000"
|
||||
|
||||
local tags = { configuration = "import_configuration_provider" }
|
||||
|
||||
local Priority = {
|
||||
ERROR = 0,
|
||||
WARNING = 1,
|
||||
INFO = 2,
|
||||
DEBUG = 3
|
||||
}
|
||||
|
||||
---@type fun(text: string)
|
||||
local printFunction = printToAll
|
||||
local printPriority = Priority.INFO
|
||||
|
||||
---@param priority number
|
||||
---@return string
|
||||
function Priority.getLabel(priority)
|
||||
if priority==0 then return "ERROR"
|
||||
elseif priority==1 then return "WARNING"
|
||||
elseif priority==2 then return "INFO"
|
||||
elseif priority==3 then return "DEBUG"
|
||||
else error(table.concat({"Priority", priority, "not found"}, " ")) return ""
|
||||
end
|
||||
end
|
||||
|
||||
---@param message string
|
||||
---@param priority number
|
||||
local function debugPrint(message, priority, color)
|
||||
if (color == nil) then
|
||||
color = { 0.5, 0.5, 0.5 }
|
||||
end
|
||||
if (printPriority >= priority) then
|
||||
printFunction("[" .. Priority.getLabel(priority) .. "] " .. message, color)
|
||||
end
|
||||
end
|
||||
|
||||
---@param str string
|
||||
---@return string
|
||||
local function fixUtf16String(str)
|
||||
return str:gsub("\\u(%w%w%w%w)", function (match)
|
||||
return string.char(tonumber(match,16))
|
||||
end)
|
||||
end
|
||||
|
||||
--Forward declaration
|
||||
---@type Request
|
||||
local Request = {}
|
||||
|
||||
---@type table<string, ArkhamImportTaboo>
|
||||
local tabooList = {}
|
||||
|
||||
---@return ArkhamImportConfiguration
|
||||
local function getConfiguration()
|
||||
local configuration = getObjectsWithTag(tags.configuration)[1]:getTable("configuration")
|
||||
printPriority = configuration.priority
|
||||
return configuration
|
||||
end
|
||||
|
||||
function onLoad(script_state)
|
||||
local state = JSON.decode(script_state)
|
||||
initializeUi(state)
|
||||
math.randomseed(os.time())
|
||||
|
||||
local configuration = getConfiguration()
|
||||
Request.start({configuration.api_uri, configuration.taboo}, function (status)
|
||||
local json = JSON.decode(fixUtf16String(status.text))
|
||||
for _, taboo in pairs(json) do
|
||||
---@type <string, boolean>
|
||||
local cards = {}
|
||||
|
||||
for _, card in pairs(JSON.decode(taboo.cards)) do
|
||||
cards[card.code] = true
|
||||
end
|
||||
|
||||
tabooList[taboo.id] = {
|
||||
date = taboo.date_start,
|
||||
cards = cards
|
||||
}
|
||||
end
|
||||
return true, nil
|
||||
end)
|
||||
end
|
||||
|
||||
function onSave()
|
||||
return JSON.encode(getUiState())
|
||||
end
|
||||
|
||||
-- Callback when the deck information is received from ArkhamDB. Parses the
|
||||
-- response then applies standard transformations to the deck such as adding
|
||||
-- random weaknesses and checking for taboos. Once the deck is processed,
|
||||
-- passes to loadCards to actually spawn the defined deck.
|
||||
---@param deck ArkhamImportDeck
|
||||
---@param playerColor String Color name of the player mat to place this deck
|
||||
-- on (e.g. "Red")
|
||||
---@param configuration ArkhamImportConfiguration
|
||||
local function onDeckResult(deck, playerColor, configuration)
|
||||
-- Load the next deck in the upgrade path if the option is enabled
|
||||
if (getUiState().loadNewest and deck.next_deck ~= nil and deck.next_deck ~= "") then
|
||||
buildDeck(playerColor, deck.next_deck)
|
||||
return
|
||||
end
|
||||
|
||||
debugPrint(table.concat({ "Found decklist: ", deck.name}), Priority.INFO, playerColor)
|
||||
|
||||
debugPrint(table.concat({"-", deck.name, "-"}), Priority.DEBUG)
|
||||
for k,v in pairs(deck) do
|
||||
if type(v)=="table" then
|
||||
debugPrint(table.concat {k, ": <table>"}, Priority.DEBUG)
|
||||
else
|
||||
debugPrint(table.concat {k, ": ", tostring(v)}, Priority.DEBUG)
|
||||
end
|
||||
end
|
||||
debugPrint("", Priority.DEBUG)
|
||||
|
||||
-- Initialize deck slot table and perform common transformations. The order
|
||||
-- of these should not be changed, as later steps may act on cards added in
|
||||
-- each. For example, a random weakness or investigator may have bonded
|
||||
-- cards or taboo entries, and should be present
|
||||
local slots = deck.slots
|
||||
maybeDrawRandomWeakness(slots, playerColor, configuration)
|
||||
maybeAddInvestigatorCards(deck, slots)
|
||||
extractBondedCards(slots, configuration)
|
||||
checkTaboos(deck.taboo_id, slots, playerColor, configuration)
|
||||
|
||||
local commandManager = getObjectFromGUID(configuration.command_manager_guid)
|
||||
|
||||
---@type ArkhamImport_CommandManager_InitializationArguments
|
||||
local parameters = {
|
||||
configuration = configuration,
|
||||
description = deck.description_md,
|
||||
}
|
||||
|
||||
---@type ArkhamImport_CommandManager_InitializationResults
|
||||
local results = commandManager:call("initialize", parameters)
|
||||
|
||||
if not results.is_successful then
|
||||
debugPrint(results.error_message, Priority.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
loadCards(slots, playerColor, commandManager, configuration, results.configuration)
|
||||
end
|
||||
|
||||
-- Checks to see if the slot list includes the random weakness ID. If it does,
|
||||
-- removes it from the deck and replaces it with the ID of a random basic
|
||||
-- weakness provided by the all cards bag
|
||||
-- Param slots: The slot list for cards in this deck. Table key is the cardId,
|
||||
-- value is the number of those cards which will be spawned
|
||||
-- Param playerColor: Color name of the player this deck is being loaded for.
|
||||
-- Used for broadcast if a weakness is added.
|
||||
-- Param configuration: The API configuration object
|
||||
function maybeDrawRandomWeakness(slots, playerColor, configuration)
|
||||
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
|
||||
local hasRandomWeakness = false
|
||||
for cardId, cardCount in pairs(slots) do
|
||||
if (cardId == RANDOM_WEAKNESS_ID) then
|
||||
hasRandomWeakness = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if (hasRandomWeakness) then
|
||||
local weaknessId = allCardsBag.call("getRandomWeaknessId")
|
||||
slots[weaknessId] = 1
|
||||
slots[RANDOM_WEAKNESS_ID] = nil
|
||||
debugPrint("Random basic weakness added to deck", Priority.INFO, playerColor)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- If the UI indicates that investigator cards should be loaded, add both the
|
||||
-- investigator (XXXXX) and minicard (XXXXX-m) slots with one copy each
|
||||
-- Param deck: The processed ArkhamDB deck response
|
||||
-- Param slots: The slot list for cards in this deck. Table key is the cardId,
|
||||
-- value is the number of those cards which will be spawned
|
||||
function maybeAddInvestigatorCards(deck, slots)
|
||||
if (getUiState().investigators) then
|
||||
local investigatorId = deck.investigator_code
|
||||
slots[investigatorId.."-m"] = 1
|
||||
local parallelFront = deck.meta ~= nil and deck.meta.alternate_front ~= nil and deck.meta.alternate_front ~= ""
|
||||
local parallelBack = deck.meta ~= nil and deck.meta.alternate_back ~= nil and deck.meta.alternate_back ~= ""
|
||||
if (parallelFront and parallelBack) then
|
||||
investigatorId = investigatorId.."-p"
|
||||
elseif (parallelFront) then
|
||||
investigatorId = investigatorId.."-pf"
|
||||
elseif (parallelBack) then
|
||||
investigatorId = investigatorId.."-pb"
|
||||
end
|
||||
slots[investigatorId] = 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Process the slot list and looks for any cards which are bonded to those in
|
||||
-- the deck. Adds those cards to the slot list.
|
||||
-- Param slots: The slot list for cards in this deck. Table key is the cardId,
|
||||
-- value is the number of those cards which will be spawned
|
||||
-- Param configuration: The API configuration object
|
||||
function extractBondedCards(slots, configuration)
|
||||
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
|
||||
-- Create a list of bonded cards first so we don't modify slots while iterating
|
||||
local bondedCards = { }
|
||||
for cardId, cardCount in pairs(slots) do
|
||||
local card = allCardsBag.call("getCardById", { id = cardId })
|
||||
if (card ~= nil and card.metadata.bonded ~= nil) then
|
||||
for _, bond in ipairs(card.metadata.bonded) do
|
||||
bondedCards[bond.id] = bond.count
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Add any bonded cards to the main slots list
|
||||
for bondedId, bondedCount in pairs(bondedCards) do
|
||||
slots[bondedId] = bondedCount
|
||||
end
|
||||
end
|
||||
|
||||
-- Check the deck for any cards on its taboo list. If they're found, replace
|
||||
-- the entry in the slot with the Taboo id (i.e. "XXXX" becomes "XXXX-t")
|
||||
-- Param tabooId: The deck's taboo ID, taken from the deck response taboo_id
|
||||
-- field. May be nil, indicating that no taboo list should be used
|
||||
-- Param slots: The slot list for cards in this deck. Table key is the cardId,
|
||||
-- value is the number of those cards which will be spawned
|
||||
function checkTaboos(tabooId, slots, playerColor, configuration)
|
||||
if (tabooId) then
|
||||
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
|
||||
for cardId, _ in pairs(tabooList[tabooId].cards) do
|
||||
if (slots[cardId] ~= nil) then
|
||||
-- Make sure there's a taboo version of the card before we replace it
|
||||
-- SCED only maintains the most recent taboo cards. If a deck is using
|
||||
-- an older taboo list it's possible the card isn't a taboo any more
|
||||
local tabooCard = allCardsBag.call("getCardById", { id = cardId.."-t" })
|
||||
if (tabooCard == nil) then
|
||||
local basicCard = allCardsBag.call("getCardById", { id = cardId })
|
||||
debugPrint("Taboo version for "..basicCard.data.Nickname..
|
||||
" is not available. Using standard version", Priority.WARNING, playerColor)
|
||||
else
|
||||
slots[cardId.."-t"] = slots[cardId]
|
||||
slots[cardId] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Process the slot list, which defines the card Ids and counts of cards to
|
||||
-- load. Spawn those cards at the appropriate zones, and report an error to the
|
||||
-- user if any could not be loaded.
|
||||
--
|
||||
-- This method uses an encapsulated coroutine with yields to make the card
|
||||
-- spawning cleaner.
|
||||
--
|
||||
-- Param slots: Key-Value table of cardId:count. cardId is the ArkhamDB ID of
|
||||
-- the card to spawn, and count is the number which should be spawned
|
||||
-- Param playerColor String Color name of the player mat to place this deck
|
||||
-- on (e.g. "Red")
|
||||
-- Param commandManager
|
||||
-- Param configuration: Loader configuration object
|
||||
-- Param command_config:
|
||||
function loadCards(slots, playerColor, commandManager, configuration, command_config)
|
||||
function coinside()
|
||||
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
|
||||
local yPos = { }
|
||||
local cardsToSpawn = { }
|
||||
for cardId, cardCount in pairs(slots) do
|
||||
local card = allCardsBag.call("getCardById", { id = cardId })
|
||||
if (card ~= nil) then
|
||||
local cardZone = Zones.getDefaultCardZone(card.metadata)
|
||||
for i = 1, cardCount do
|
||||
table.insert(cardsToSpawn, { data = card.data, metadata = card.metadata, zone = cardZone })
|
||||
end
|
||||
slots[cardId] = 0
|
||||
end
|
||||
end
|
||||
|
||||
-- TODO: Re-enable this later, as a command
|
||||
--handleAltInvestigatorCard(cardsToSpawn, "promo", configuration)
|
||||
|
||||
table.sort(cardsToSpawn, cardComparator)
|
||||
|
||||
-- TODO: Process commands for the cardsToSpawn list
|
||||
|
||||
-- These should probably be commands, once the command handler is updated
|
||||
handleStartsInPlay(cardsToSpawn)
|
||||
handleAncestralKnowledge(cardsToSpawn)
|
||||
|
||||
-- Count the number of cards in each zone so we know if it's a deck or card.
|
||||
-- TTS's Card vs. Deck distinction requires this since we can't spawn a deck
|
||||
-- with only one card
|
||||
local zoneCounts = getZoneCounts(cardsToSpawn)
|
||||
local zoneDecks = { }
|
||||
for zone, count in pairs(zoneCounts) do
|
||||
if (count > 1) then
|
||||
zoneDecks[zone] = buildDeckDataTemplate()
|
||||
end
|
||||
end
|
||||
-- For each card in a deck zone, add it to that deck. Otherwise, spawn it
|
||||
-- directly
|
||||
for _, spawnCard in ipairs(cardsToSpawn) do
|
||||
if (zoneDecks[spawnCard.zone] ~= nil) then
|
||||
addCardToDeck(zoneDecks[spawnCard.zone], spawnCard.data)
|
||||
else
|
||||
local cardPos = Zones.getZonePosition(playerColor, spawnCard.zone)
|
||||
cardPos.y = 2
|
||||
spawnObjectData({
|
||||
data = spawnCard.data,
|
||||
position = cardPos,
|
||||
rotation = Zones.getDefaultCardRotation(playerColor, spawnCard.zone)})
|
||||
end
|
||||
end
|
||||
-- Spawn each of the decks
|
||||
for zone, deck in pairs(zoneDecks) do
|
||||
local deckPos = Zones.getZonePosition(playerColor, zone)
|
||||
deckPos.y = 3
|
||||
spawnObjectData({
|
||||
data = deck,
|
||||
position = deckPos,
|
||||
rotation = Zones.getDefaultCardRotation(playerColor, zone)})
|
||||
coroutine.yield(0)
|
||||
end
|
||||
|
||||
-- Look for any cards which haven't been loaded
|
||||
local hadError = false
|
||||
for cardId, remainingCount in pairs(slots) do
|
||||
if (remainingCount > 0) then
|
||||
hadError = true
|
||||
local request = Request.start({
|
||||
configuration.api_uri,
|
||||
configuration.cards,
|
||||
cardId},
|
||||
function(result)
|
||||
local adbCardInfo = JSON.decode(fixUtf16String(result.text))
|
||||
local cardName = adbCardInfo.real_name
|
||||
if (cardName ~= nil) then
|
||||
if (adbCardInfo.xp ~= nil and adbCardInfo.xp > 0) then
|
||||
cardName = cardName.." ("..adbCardInfo.xp..")"
|
||||
end
|
||||
debugPrint("Card not found: "..cardName..", ArkhamDB ID "..cardId, Priority.ERROR, playerColor)
|
||||
else
|
||||
debugPrint("Card not found in ArkhamDB, ID "..cardId, Priority.ERROR, playerColor)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
if (not hadError) then
|
||||
debugPrint("Deck loaded successfully!", Priority.INFO, playerColor)
|
||||
end
|
||||
return 1
|
||||
end
|
||||
startLuaCoroutine(self, "coinside")
|
||||
end
|
||||
|
||||
-- Inserts a card into the given deck. This does three things:
|
||||
-- 1. Add the card's data to ContainedObjects
|
||||
-- 2. Add the card's ID (the TTS CardID, not the Arkham ID) to the deck's
|
||||
-- ID list. Note that the deck's ID list is "DeckIDs" even though it
|
||||
-- contains a list of card Ids
|
||||
-- 3. Extract the card's CustomDeck table and add it to the deck. The deck's
|
||||
-- "CustomDeck" field is a list of all CustomDecks used by cards within the
|
||||
-- deck, keyed by the DeckID and referencing the custom deck table
|
||||
-- Param deck: TTS deck data structure to add to
|
||||
-- Param card: Data for the card to be inserted
|
||||
function addCardToDeck(deck, cardData)
|
||||
table.insert(deck.ContainedObjects, cardData)
|
||||
table.insert(deck.DeckIDs, cardData.CardID)
|
||||
for customDeckId, customDeckData in pairs(cardData.CustomDeck) do
|
||||
deck.CustomDeck[customDeckId] = customDeckData
|
||||
end
|
||||
end
|
||||
|
||||
-- Count the number of cards in each zone
|
||||
-- Param cards: Table of {cardData, cardMetadata, zone}
|
||||
-- Return: Table of {zoneName=zoneCount}
|
||||
function getZoneCounts(cards)
|
||||
local counts = { }
|
||||
for _, card in ipairs(cards) do
|
||||
if (counts[card.zone] == nil) then
|
||||
counts[card.zone] = 1
|
||||
else
|
||||
counts[card.zone] = counts[card.zone] + 1
|
||||
end
|
||||
end
|
||||
|
||||
return counts
|
||||
end
|
||||
|
||||
-- Create an empty deck data table which can have cards added to it. This
|
||||
-- creates a new table on each call without using metatables or previous
|
||||
-- definitions because we can't be sure that TTS doesn't modify the structure
|
||||
-- Return: Table containing the minimal TTS deck data structure
|
||||
function buildDeckDataTemplate()
|
||||
local deck = { }
|
||||
deck.Name = "Deck"
|
||||
|
||||
-- Card data. DeckIDs and CustomDeck entries will be built from the cards
|
||||
deck.ContainedObjects = { }
|
||||
deck.DeckIDs = { }
|
||||
deck.CustomDeck = { }
|
||||
|
||||
-- Transform is required, Position and Rotation will be overridden by the
|
||||
-- spawn call so can be omitted here
|
||||
deck.Transform = {
|
||||
scaleX = 1,
|
||||
scaleY = 1,
|
||||
scaleZ = 1, }
|
||||
|
||||
return deck
|
||||
end
|
||||
|
||||
-- Get the PBN (Permanent/Bonded/Normal) value from the given metadata.
|
||||
-- Return: 1 for Permanent, 2 for Bonded, or 3 for Normal. The actual values
|
||||
-- are irrelevant as they provide only grouping and the order between them
|
||||
-- doesn't matter.
|
||||
function getPbn(metadata)
|
||||
if (metadata.permanent) then
|
||||
return 1
|
||||
elseif (metadata.bonded_to ~= nil) then
|
||||
return 2
|
||||
else -- Normal card
|
||||
return 3
|
||||
end
|
||||
end
|
||||
|
||||
-- Comparison function used to sort the cards in a deck. Groups bonded or
|
||||
-- permanent cards first, then sorts within theose types by name/subname.
|
||||
-- Normal cards will sort in standard alphabetical order, while permanent/bonded
|
||||
-- will be in reverse alphabetical order.
|
||||
--
|
||||
-- Since cards spawn in the order provided by this comparator, with the first
|
||||
-- cards ending up at the bottom of a pile, this ordering will spawn in reverse
|
||||
-- alphabetical order. This presents the cards in order for non-face-down
|
||||
-- areas, and presents them in order when Searching the face-down deck.
|
||||
function cardComparator(card1, card2)
|
||||
local pbn1 = getPbn(card1.metadata)
|
||||
local pbn2 = getPbn(card2.metadata)
|
||||
if (pbn1 ~= pbn2) then
|
||||
return pbn1 > pbn2
|
||||
end
|
||||
if (pbn1 == 3) then
|
||||
if (card1.data.Nickname ~= card2.data.Nickname) then
|
||||
return card1.data.Nickname < card2.data.Nickname
|
||||
end
|
||||
return card1.data.Description < card2.data.Description
|
||||
else
|
||||
if (card1.data.Nickname ~= card2.data.Nickname) then
|
||||
return card1.data.Nickname > card2.data.Nickname
|
||||
end
|
||||
return card1.data.Description > card2.data.Description
|
||||
end
|
||||
end
|
||||
|
||||
-- Replace the investigator card and minicard with an alternate version. This
|
||||
-- will find the relevant cards and look for IDs with <id>-<altVersionTag>, and
|
||||
-- <id>-<altVersionTag>-m, and update the entries in cardList with the new card
|
||||
-- data.
|
||||
--
|
||||
-- Param cardList: Deck list being created
|
||||
-- Param altVersionTag: The tag for the different version, currently the only
|
||||
-- alt versions are "promo", but will soon inclide "revised"
|
||||
-- Param configuration: ArkhamDB configuration defniition, used for the card bag
|
||||
function handleAltInvestigatorCard(cardList, altVersionTag, configuration)
|
||||
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
|
||||
for _, card in ipairs(cardList) do
|
||||
if (card.metadata.type == "Investigator") then
|
||||
local altInvestigator = allCardsBag.call("getCardById", { id = card.metadata.id.."-"..altVersionTag})
|
||||
if (altInvestigator ~= nil) then
|
||||
card.data = altInvestigator.data
|
||||
card.metadata = altInvestigator.metadata
|
||||
end
|
||||
end
|
||||
if (card.metadata.type == "Minicard") then
|
||||
-- -promo comes before -m in the ID, so needs a little massaging
|
||||
local investigatorId = string.sub(card.metadata.id, 1, 5)
|
||||
local altMinicard = allCardsBag.call("getCardById", { id = investigatorId.."-"..altVersionTag.."-m"})
|
||||
if (altMinicard ~= nil) then
|
||||
card.data = altMinicard.data
|
||||
card.metadata = altMinicard.metadata
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Place cards which start in play (Duke, Sophie) in the play area
|
||||
function handleStartsInPlay(cardList)
|
||||
for _, card in ipairs(cardList) do
|
||||
-- 02014 = Duke (Ashcan Pete)
|
||||
-- 03009 = Sophie (Mark Harrigan)
|
||||
if (card.metadata.id == "02014" or card.metadata.id == "03009") then
|
||||
card.zone = "BlankTop"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check to see if the deck list has Ancestral Knowledge. If it does, move 5
|
||||
-- random skills to SetAside3
|
||||
function handleAncestralKnowledge(cardList)
|
||||
local hasAncestralKnowledge = false
|
||||
local skillList = { }
|
||||
-- Have to process the entire list to check for Ancestral Knowledge and get
|
||||
-- all possible skills, so do both in one pass
|
||||
for i, card in ipairs(cardList) do
|
||||
if (card.metadata.id == "07303") then
|
||||
-- Ancestral Knowledge found
|
||||
hasAncestralKnowledge = true
|
||||
card.zone = "SetAside3"
|
||||
elseif (card.metadata.type == "Skill"
|
||||
and card.metadata.bonded_to == nil
|
||||
and not card.metadata.weakness) then
|
||||
table.insert(skillList, i)
|
||||
end
|
||||
end
|
||||
if (hasAncestralKnowledge) then
|
||||
for i = 1,5 do
|
||||
-- Move 5 random skills to SetAside3
|
||||
local skillListIndex = math.random(#skillList)
|
||||
cardList[skillList[skillListIndex]].zone = "UnderSetAside3"
|
||||
table.remove(skillList, skillListIndex)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Test method. Loads all decks which were submitted to ArkhamDB on a given
|
||||
-- date window.
|
||||
function testLoadLotsOfDecks()
|
||||
local configuration = getConfiguration()
|
||||
local numDays = 7
|
||||
local day = os.time{year=2021, month=7, day=15} -- Start date here
|
||||
for i=1,numDays do
|
||||
local dateString = os.date("%Y-%m-%d", day)
|
||||
local deckList = Request.start({
|
||||
configuration.api_uri,
|
||||
"decklists/by_date",
|
||||
dateString,
|
||||
},
|
||||
function(result)
|
||||
local json = JSON.decode(result.text)
|
||||
for i, deckData in ipairs(json) do
|
||||
buildDeck(getColorForTest(i), deckData.id)
|
||||
end
|
||||
end)
|
||||
day = day + (60 * 60 * 24) -- Move forward by one day
|
||||
end
|
||||
end
|
||||
|
||||
-- Rotates the player mat based on index, to spread the card stacks during
|
||||
-- a mass load
|
||||
function getColorForTest(index)
|
||||
if (index % 4 == 0) then
|
||||
return "Red"
|
||||
elseif (index % 4 == 1) then
|
||||
return "Orange"
|
||||
elseif (index % 4 == 2) then
|
||||
return "White"
|
||||
elseif (index % 4 == 3) then
|
||||
return "Green"
|
||||
end
|
||||
end
|
||||
|
||||
-- Start the deck build process for the given player color and deck ID. This
|
||||
-- will retrieve the deck from ArkhamDB, and pass to a callback for processing.
|
||||
-- Param playerColor String Color name of the player mat to place this deck
|
||||
-- on (e.g. "Red")
|
||||
-- Param deckId: ArkhamDB deck id to be loaded
|
||||
function buildDeck(playerColor, deckId)
|
||||
local configuration = getConfiguration()
|
||||
-- Get a simple card to see if the bag indexes are complete. If not, abort
|
||||
-- the deck load. The called method will handle player notification.
|
||||
local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
|
||||
local checkCard = allCardsBag.call("getCardById", { id = "01001"})
|
||||
if (checkCard ~= nil and checkCard.data == nil) then
|
||||
return
|
||||
end
|
||||
|
||||
local deckUri = { configuration.api_uri, getUiState().private and configuration.private_deck or configuration.public_deck, deckId }
|
||||
|
||||
local deck = Request.start(deckUri, function (status)
|
||||
if string.find(status.text, "<!DOCTYPE html>") then
|
||||
debugPrint("Private deck ID "..deckId.." is not shared", Priority.ERROR, playerColor)
|
||||
return false, table.concat({ "Private deck ", deckId, " is not shared"})
|
||||
end
|
||||
local json = JSON.decode(status.text)
|
||||
|
||||
if not json then
|
||||
debugPrint("Deck ID "..deckId.." not found", Priority.ERROR, playerColor)
|
||||
return false, "Deck not found!"
|
||||
end
|
||||
|
||||
return true, JSON.decode(status.text)
|
||||
end)
|
||||
|
||||
deck:with(onDeckResult, playerColor, configuration)
|
||||
end
|
||||
|
||||
---@type Request
|
||||
Request = {
|
||||
is_done = false,
|
||||
is_successful = false
|
||||
}
|
||||
|
||||
--- Creates a new instance of a Request. Should not be directly called. Instead use Request.start and Request.deferred.
|
||||
---@param uri string
|
||||
---@param configure fun(request: Request, status: WebRequestStatus)
|
||||
---@return Request
|
||||
function Request:new(uri, configure)
|
||||
local this = {}
|
||||
|
||||
setmetatable(this, self)
|
||||
self.__index = self
|
||||
|
||||
if type(uri)=="table" then
|
||||
uri = table.concat(uri, "/")
|
||||
end
|
||||
|
||||
this.uri = uri
|
||||
|
||||
WebRequest.get(uri, function(status)
|
||||
configure(this, status)
|
||||
end)
|
||||
|
||||
return this
|
||||
end
|
||||
|
||||
--- Creates a new request. on_success should set the request's is_done, is_successful, and content variables.
|
||||
--- Deferred should be used when you don't want to set is_done immediately (such as if you want to wait for another request to finish)
|
||||
---@param uri string
|
||||
---@param on_success fun(request: Request, status: WebRequestStatus, vararg any)
|
||||
---@param on_error fun(status: WebRequestStatus)|nil
|
||||
---@vararg any[]
|
||||
---@return Request
|
||||
function Request.deferred(uri, on_success, on_error, ...)
|
||||
local parameters = table.pack(...)
|
||||
return Request:new(uri, function (request, status)
|
||||
if (status.is_done) then
|
||||
if (status.is_error) then
|
||||
request.error_message = on_error and on_error(status, table.unpack(parameters)) or status.error
|
||||
request.is_successful = false
|
||||
request.is_done = true
|
||||
else
|
||||
on_success(request, status)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Creates a new request. on_success should return weather the resultant data is as expected, and the processed content of the request.
|
||||
---@param uri string
|
||||
---@param on_success fun(status: WebRequestStatus, vararg any): boolean, any
|
||||
---@param on_error nil|fun(status: WebRequestStatus, vararg any): string
|
||||
---@vararg any[]
|
||||
---@return Request
|
||||
function Request.start(uri, on_success, on_error, ...)
|
||||
local parameters = table.pack(...)
|
||||
return Request.deferred(uri, function(request, status)
|
||||
local result, message = on_success(status, table.unpack(parameters))
|
||||
if not result then request.error_message = message else request.content = message end
|
||||
request.is_successful = result
|
||||
request.is_done = true
|
||||
end, on_error, table.unpack(parameters))
|
||||
end
|
||||
|
||||
---@param requests Request[]
|
||||
---@param on_success fun(content: any[], vararg any[])
|
||||
---@param on_error fun(requests: Request[], vararg any[])|nil
|
||||
---@vararg any
|
||||
function Request.with_all(requests, on_success, on_error, ...)
|
||||
local parameters = table.pack(...)
|
||||
|
||||
Wait.condition(function ()
|
||||
---@type any[]
|
||||
local results = {}
|
||||
|
||||
---@type Request[]
|
||||
local errors = {}
|
||||
|
||||
for _, request in ipairs(requests) do
|
||||
if request.is_successful then
|
||||
table.insert(results, request.content)
|
||||
else
|
||||
table.insert(errors, request)
|
||||
end
|
||||
end
|
||||
|
||||
if (#errors<=0) then
|
||||
on_success(results, table.unpack(parameters))
|
||||
elseif on_error ==nil then
|
||||
for _, request in ipairs(errors) do
|
||||
debugPrint(table.concat({ "[ERROR]", request.uri, ":", request.error_message }), Priority.ERROR)
|
||||
end
|
||||
else
|
||||
on_error(requests, table.unpack(parameters))
|
||||
end
|
||||
end, function ()
|
||||
for _, request in ipairs(requests) do
|
||||
if not request.is_done then return false end
|
||||
end
|
||||
return true
|
||||
end)
|
||||
end
|
||||
|
||||
---@param callback fun(content: any, vararg any)
|
||||
function Request:with(callback, ...)
|
||||
local arguments = table.pack(...)
|
||||
Wait.condition(function ()
|
||||
if self.is_successful then
|
||||
callback(self.content, table.unpack(arguments))
|
||||
end
|
||||
end, function () return self.is_done
|
||||
end)
|
||||
end
|
12
src/arkhamdb/HotfixBag.ttslua
Normal file
12
src/arkhamdb/HotfixBag.ttslua
Normal file
@ -0,0 +1,12 @@
|
||||
-- A Hotfix bag contains replacement cards for the All Cards Bag, and should
|
||||
-- have the 'AllCardsHotfix' tag on the object. Code for the All Cards Bag will
|
||||
-- find these bags during indexing, and use them to replace cards from the
|
||||
-- actual bag.
|
||||
|
||||
-- Tells the All Cards Bag to recreate its indexes. The All Cards Bag may
|
||||
-- ignore this request; see the rebuildIndexForHotfix() method in the All Cards
|
||||
-- Bag for details.
|
||||
function onLoad()
|
||||
local allCardsBag = getObjectFromGUID("15bb07")
|
||||
allCardsBag.call("rebuildIndexForHotfix")
|
||||
end
|
258
src/arkhamdb/LoaderUi.ttslua
Normal file
258
src/arkhamdb/LoaderUi.ttslua
Normal file
@ -0,0 +1,258 @@
|
||||
local INPUT_FIELD_HEIGHT = 340
|
||||
local INPUT_FIELD_WIDTH = 1500
|
||||
|
||||
local FIELD_COLOR = {0.9,0.7,0.5}
|
||||
|
||||
local PRIVATE_TOGGLE_LABELS = { }
|
||||
PRIVATE_TOGGLE_LABELS[true] = "Private"
|
||||
PRIVATE_TOGGLE_LABELS[false] = "Published"
|
||||
local UPGRADED_TOGGLE_LABELS = { }
|
||||
UPGRADED_TOGGLE_LABELS[true] = "Upgraded"
|
||||
UPGRADED_TOGGLE_LABELS[false] = "Specific"
|
||||
local LOAD_INVESTIGATOR_TOGGLE_LABELS = { }
|
||||
LOAD_INVESTIGATOR_TOGGLE_LABELS[true] = "Yes"
|
||||
LOAD_INVESTIGATOR_TOGGLE_LABELS[false] = "No"
|
||||
|
||||
local redDeckId = ""
|
||||
local orangeDeckId = ""
|
||||
local whiteDeckId = ""
|
||||
local greenDeckId = ""
|
||||
local privateDeck = true
|
||||
local loadNewestDeck = true
|
||||
local loadInvestigators = false
|
||||
local loadingColor = ""
|
||||
|
||||
-- Returns a table with the full state of the UI, including options and deck
|
||||
-- IDs. This can be used to persist via onSave(), or provide values for a load
|
||||
-- operation
|
||||
-- Table values:
|
||||
-- redDeck: Deck ID to load for the red player
|
||||
-- orangeDeck: Deck ID to load for the orange player
|
||||
-- whiteDeck: Deck ID to load for the white player
|
||||
-- greenDeck: Deck ID to load for the green player
|
||||
-- private: True to load a private deck, false to load a public deck
|
||||
-- loadNewest: True if the most upgraded version of the deck should be loaded
|
||||
-- investigators: True if investigator cards should be spawned
|
||||
function getUiState()
|
||||
return {
|
||||
redDeck = redDeckId,
|
||||
orangeDeck = orangeDeckId,
|
||||
whiteDeck = whiteDeckId,
|
||||
greenDeck = greenDeckId,
|
||||
private = privateDeck,
|
||||
loadNewest = loadNewestDeck,
|
||||
investigators = loadInvestigators,
|
||||
}
|
||||
end
|
||||
|
||||
-- Sets up the UI for the deck loader, populating fields from the given save
|
||||
-- state table decoded from onLoad()
|
||||
function initializeUi(savedUiState)
|
||||
if (savedUiState ~= nil) then
|
||||
redDeckId = savedUiState.redDeck
|
||||
orangeDeckId = savedUiState.orangeDeck
|
||||
whiteDeckId = savedUiState.whiteDeck
|
||||
greenDeckId = savedUiState.greenDeck
|
||||
privateDeck = savedUiState.private
|
||||
loadNewestDeck = savedUiState.loadNewest
|
||||
loadInvestigators = savedUiState.investigators
|
||||
else
|
||||
redDeckId = ""
|
||||
orangeDeckId = ""
|
||||
whiteDeckId = ""
|
||||
greenDeckId = ""
|
||||
privateDeck = true
|
||||
loadNewestDeck = true
|
||||
loadInvestigators = true
|
||||
end
|
||||
|
||||
makeOptionToggles()
|
||||
makeDeckIdFields()
|
||||
makeBuildButton()
|
||||
end
|
||||
|
||||
function makeOptionToggles()
|
||||
-- Creates the three option toggle buttons. Each toggle assumes its index as
|
||||
-- part of the toggle logic. IF YOU CHANGE THE ORDER OF THESE FIELDS YOU MUST
|
||||
-- CHANGE THE EVENT HANDLERS
|
||||
makePublicPrivateToggle()
|
||||
makeLoadUpgradedToggle()
|
||||
makeLoadInvestigatorsToggle()
|
||||
end
|
||||
|
||||
function makePublicPrivateToggle()
|
||||
local checkbox_parameters = {}
|
||||
checkbox_parameters.click_function = "publicPrivateChanged"
|
||||
checkbox_parameters.function_owner = self
|
||||
checkbox_parameters.position = {0.25,0.1,-0.102}
|
||||
checkbox_parameters.width = INPUT_FIELD_WIDTH
|
||||
checkbox_parameters.height = INPUT_FIELD_HEIGHT
|
||||
checkbox_parameters.tooltip = "Published or private deck.\n\n*****PLEASE USE A PRIVATE DECK IF JUST FOR TTS TO AVOID FLOODING ARKHAMDB PUBLISHED DECK LISTS!"
|
||||
checkbox_parameters.label = PRIVATE_TOGGLE_LABELS[privateDeck]
|
||||
checkbox_parameters.font_size = 240
|
||||
checkbox_parameters.scale = {0.1,0.1,0.1}
|
||||
checkbox_parameters.color = FIELD_COLOR
|
||||
checkbox_parameters.hover_color = {0.4,0.6,0.8}
|
||||
self.createButton(checkbox_parameters)
|
||||
end
|
||||
|
||||
function makeLoadUpgradedToggle()
|
||||
local checkbox_parameters = {}
|
||||
checkbox_parameters.click_function = "loadUpgradedChanged"
|
||||
checkbox_parameters.function_owner = self
|
||||
checkbox_parameters.position = {0.25,0.1,-0.01}
|
||||
checkbox_parameters.width = INPUT_FIELD_WIDTH
|
||||
checkbox_parameters.height = INPUT_FIELD_HEIGHT
|
||||
checkbox_parameters.tooltip = "Load newest upgrade, or exact deck"
|
||||
checkbox_parameters.label = UPGRADED_TOGGLE_LABELS[loadNewestDeck]
|
||||
checkbox_parameters.font_size = 240
|
||||
checkbox_parameters.scale = {0.1,0.1,0.1}
|
||||
checkbox_parameters.color = FIELD_COLOR
|
||||
checkbox_parameters.hover_color = {0.4,0.6,0.8}
|
||||
self.createButton(checkbox_parameters)
|
||||
end
|
||||
|
||||
function makeLoadInvestigatorsToggle()
|
||||
local checkbox_parameters = {}
|
||||
checkbox_parameters.click_function = "loadInvestigatorsChanged"
|
||||
checkbox_parameters.function_owner = self
|
||||
checkbox_parameters.position = {0.25,0.1,0.081}
|
||||
checkbox_parameters.width = INPUT_FIELD_WIDTH
|
||||
checkbox_parameters.height = INPUT_FIELD_HEIGHT
|
||||
checkbox_parameters.tooltip = "Spawn investigator cards?"
|
||||
checkbox_parameters.label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators]
|
||||
checkbox_parameters.font_size = 240
|
||||
checkbox_parameters.scale = {0.1,0.1,0.1}
|
||||
checkbox_parameters.color = FIELD_COLOR
|
||||
checkbox_parameters.hover_color = {0.4,0.6,0.8}
|
||||
self.createButton(checkbox_parameters)
|
||||
end
|
||||
|
||||
-- Create the four deck ID entry fields
|
||||
function makeDeckIdFields()
|
||||
local input_parameters = {}
|
||||
-- Parameters common to all entry fields
|
||||
input_parameters.function_owner = self
|
||||
input_parameters.scale = {0.1,0.1,0.1}
|
||||
input_parameters.width = INPUT_FIELD_WIDTH
|
||||
input_parameters.height = INPUT_FIELD_HEIGHT
|
||||
input_parameters.font_size = 320
|
||||
input_parameters.tooltip = "Deck ID from ArkhamDB URL of the deck\nPublic URL: 'https://arkhamdb.com/decklist/view/101/knowledge-overwhelming-solo-deck-1.0' = '101'\nPrivate URL: 'https://arkhamdb.com/deck/view/102' = '102'"
|
||||
input_parameters.alignment = 3 -- Center
|
||||
input_parameters.color = FIELD_COLOR
|
||||
input_parameters.font_color = {0, 0, 0}
|
||||
input_parameters.validation = 2 -- Integer
|
||||
|
||||
-- Green
|
||||
input_parameters.input_function = "greenDeckChanged"
|
||||
input_parameters.position = {-0.166,0.1,0.385}
|
||||
input_parameters.value=greenDeckId
|
||||
self.createInput(input_parameters)
|
||||
-- Red
|
||||
input_parameters.input_function = "redDeckChanged"
|
||||
input_parameters.position = {0.171,0.1,0.385}
|
||||
input_parameters.value=redDeckId
|
||||
self.createInput(input_parameters)
|
||||
-- White
|
||||
input_parameters.input_function = "whiteDeckChanged"
|
||||
input_parameters.position = {-0.166,0.1,0.474}
|
||||
input_parameters.value=whiteDeckId
|
||||
self.createInput(input_parameters)
|
||||
-- Orange
|
||||
input_parameters.input_function = "orangeDeckChanged"
|
||||
input_parameters.position = {0.171,0.1,0.474}
|
||||
input_parameters.value=orangeDeckId
|
||||
self.createInput(input_parameters)
|
||||
end
|
||||
|
||||
-- Create the Build All button. This is a transparent button which covers the
|
||||
-- Build All portion of the background graphic
|
||||
function makeBuildButton()
|
||||
local button_parameters = {}
|
||||
button_parameters.click_function = "loadDecks"
|
||||
button_parameters.function_owner = self
|
||||
button_parameters.position = {0,0.1,0.71}
|
||||
button_parameters.width = 320
|
||||
button_parameters.height = 30
|
||||
button_parameters.color = {0, 0, 0, 0}
|
||||
button_parameters.tooltip = "Click to build all four decks!"
|
||||
self.createButton(button_parameters)
|
||||
end
|
||||
|
||||
-- Event handler for the Public/Private toggle. Changes the local value and the
|
||||
-- labels to toggle the button
|
||||
function publicPrivateChanged()
|
||||
-- editButton uses parameters.index which is 0-indexed
|
||||
privateDeck = not privateDeck
|
||||
self.editButton {
|
||||
index = 0,
|
||||
label = PRIVATE_TOGGLE_LABELS[privateDeck],
|
||||
}
|
||||
end
|
||||
|
||||
-- Event handler for the Upgraded toggle. Changes the local value and the
|
||||
-- labels to toggle the button
|
||||
function loadUpgradedChanged()
|
||||
-- editButton uses parameters.index which is 0-indexed
|
||||
loadNewestDeck = not loadNewestDeck
|
||||
self.editButton {
|
||||
index = 1,
|
||||
label = UPGRADED_TOGGLE_LABELS[loadNewestDeck],
|
||||
}
|
||||
end
|
||||
|
||||
-- Event handler for the load investigator cards toggle. Changes the local
|
||||
-- value and the labels to toggle the button
|
||||
function loadInvestigatorsChanged()
|
||||
-- editButton uses parameters.index which is 0-indexed
|
||||
loadInvestigators = not loadInvestigators
|
||||
self.editButton {
|
||||
index = 2,
|
||||
label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators],
|
||||
}
|
||||
end
|
||||
|
||||
-- Event handler for deck ID change
|
||||
function redDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected)
|
||||
redDeckId = inputValue
|
||||
end
|
||||
|
||||
-- Event handler for deck ID change
|
||||
function orangeDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected)
|
||||
orangeDeckId = inputValue
|
||||
end
|
||||
|
||||
-- Event handler for deck ID change
|
||||
function whiteDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected)
|
||||
whiteDeckId = inputValue
|
||||
end
|
||||
|
||||
-- Event handler for deck ID change
|
||||
function greenDeckChanged(objectInputTyped, playerColorTyped, inputValue, selected)
|
||||
greenDeckId = inputValue
|
||||
end
|
||||
|
||||
function loadDecks()
|
||||
-- testLoadLotsOfDecks()
|
||||
-- Method in DeckImporterMain, visible due to inclusion
|
||||
|
||||
-- TODO: Make this use the configuration ID for the all cards bag
|
||||
local allCardsBag = getObjectFromGUID("15bb07")
|
||||
local indexReady = allCardsBag.call("isIndexReady")
|
||||
if (not indexReady) then
|
||||
broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2})
|
||||
return
|
||||
end
|
||||
if (redDeckId ~= nil and redDeckId ~= "") then
|
||||
buildDeck("Red", redDeckId)
|
||||
end
|
||||
if (orangeDeckId ~= nil and orangeDeckId ~= "") then
|
||||
buildDeck("Orange", orangeDeckId)
|
||||
end
|
||||
if (whiteDeckId ~= nil and whiteDeckId ~= "") then
|
||||
buildDeck("White", whiteDeckId)
|
||||
end
|
||||
if (greenDeckId ~= nil and greenDeckId ~= "") then
|
||||
buildDeck("Green", greenDeckId)
|
||||
end
|
||||
end
|
542
src/arkhamdb/MainLogic.ttslua
Normal file
542
src/arkhamdb/MainLogic.ttslua
Normal file
@ -0,0 +1,542 @@
|
||||
---
|
||||
--- 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
|
70
src/arkhamdb/MoveCommand.ttslua
Normal file
70
src/arkhamdb/MoveCommand.ttslua
Normal file
@ -0,0 +1,70 @@
|
||||
---
|
||||
--- 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
|
91
src/arkhamdb/ProxyCardCommand.ttslua
Normal file
91
src/arkhamdb/ProxyCardCommand.ttslua
Normal file
@ -0,0 +1,91 @@
|
||||
---
|
||||
--- 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
|
96
src/arkhamdb/ProxyInvestigatorCommand.ttslua
Normal file
96
src/arkhamdb/ProxyInvestigatorCommand.ttslua
Normal file
@ -0,0 +1,96 @@
|
||||
---
|
||||
--- 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
|
24
src/arkhamdb/RandomWeaknessGenerator.ttslua
Normal file
24
src/arkhamdb/RandomWeaknessGenerator.ttslua
Normal file
@ -0,0 +1,24 @@
|
||||
local allCardsBagGuid = "15bb07"
|
||||
|
||||
function onLoad(saved_data)
|
||||
createDrawButton()
|
||||
end
|
||||
|
||||
function createDrawButton()
|
||||
self.createButton({
|
||||
label="Draw Random\nWeakness", click_function="buttonClick_draw", function_owner=self,
|
||||
position={0,0.1,2.1}, rotation={0,0,0}, height=600, width=1800,
|
||||
font_size=250, color={0,0,0}, font_color={1,1,1}
|
||||
})
|
||||
end
|
||||
|
||||
-- Draw a random weakness and spawn it below the object
|
||||
function buttonClick_draw()
|
||||
local allCardsBag = getObjectFromGUID(allCardsBagGuid)
|
||||
local weaknessId = allCardsBag.call("getRandomWeaknessId")
|
||||
local card = allCardsBag.call("getCardById", { id = weaknessId })
|
||||
spawnObjectData({
|
||||
data = card.data,
|
||||
position = self.positionToWorld({0, 1, 5.5}),
|
||||
rotation = self.getRotation()})
|
||||
end
|
171
src/arkhamdb/Zones.ttslua
Normal file
171
src/arkhamdb/Zones.ttslua
Normal file
@ -0,0 +1,171 @@
|
||||
-- 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
|
13
src/chaosbag/ChaosBag.ttslua
Normal file
13
src/chaosbag/ChaosBag.ttslua
Normal file
@ -0,0 +1,13 @@
|
||||
function filterObjectEnter(obj)
|
||||
local props = obj.getCustomObject()
|
||||
if props ~= nil and props.image ~= nil then
|
||||
obj.setName(Global.call("getTokenName", { url=props.image }))
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function onCollisionEnter(collision_info)
|
||||
self.shuffle()
|
||||
self.shuffle()
|
||||
self.shuffle()
|
||||
end
|
103
src/chaosbag/StatTracker.ttslua
Normal file
103
src/chaosbag/StatTracker.ttslua
Normal file
@ -0,0 +1,103 @@
|
||||
function onload(saved_data)
|
||||
light_mode = false
|
||||
|
||||
if saved_data ~= "" then
|
||||
local loaded_data = JSON.decode(saved_data)
|
||||
light_mode = loaded_data[1]
|
||||
end
|
||||
createAll()
|
||||
end
|
||||
|
||||
-- functions delegated to Global
|
||||
function printStats(object, player, isRightClick)
|
||||
-- local toPosition = self.positionToWorld(DRAWN_CHAOS_TOKEN_OFFSET)
|
||||
if isRightClick then
|
||||
Global.call("resetStats")
|
||||
else
|
||||
Global.call("printStats")
|
||||
end
|
||||
end
|
||||
|
||||
function updateSave()
|
||||
local data_to_save = {light_mode }
|
||||
saved_data = JSON.encode(data_to_save)
|
||||
self.script_state = saved_data
|
||||
end
|
||||
|
||||
function createAll()
|
||||
s_color = {0.5, 0.5, 0.5, 95}
|
||||
|
||||
if light_mode then
|
||||
f_color = {1,1,1,95}
|
||||
else
|
||||
f_color = {0,0,0,100}
|
||||
end
|
||||
|
||||
self.createButton({
|
||||
click_function="printStats",
|
||||
function_owner=self,
|
||||
position={0,0.05,0},
|
||||
height=600,
|
||||
width=1000,
|
||||
alignment = 3,
|
||||
tooltip = "Left Click to print stats. Right Click to reset them.",
|
||||
scale={x=1.5, y=1.5, z=1.5},
|
||||
font_size=600,
|
||||
font_color=f_color,
|
||||
color={0,0,0,0}
|
||||
})
|
||||
|
||||
if light_mode then
|
||||
lightButtonText = "[ Set dark ]"
|
||||
else
|
||||
lightButtonText = "[ Set light ]"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function removeAll()
|
||||
self.removeInput(0)
|
||||
self.removeInput(1)
|
||||
self.removeButton(0)
|
||||
self.removeButton(1)
|
||||
self.removeButton(2)
|
||||
end
|
||||
|
||||
function reloadAll()
|
||||
removeAll()
|
||||
createAll()
|
||||
updateSave()
|
||||
end
|
||||
|
||||
function swap_fcolor(_obj, _color, alt_click)
|
||||
light_mode = not light_mode
|
||||
reloadAll()
|
||||
end
|
||||
|
||||
function swap_align(_obj, _color, alt_click)
|
||||
center_mode = not center_mode
|
||||
reloadAll()
|
||||
end
|
||||
|
||||
function editName(_obj, _string, value)
|
||||
self.setName(value)
|
||||
setTooltips()
|
||||
end
|
||||
|
||||
function setTooltips()
|
||||
self.editInput({
|
||||
index = 0,
|
||||
value = self.getName(),
|
||||
tooltip = "Left click to show stats. Right click to reset them."
|
||||
})
|
||||
end
|
||||
|
||||
function keepSample(_obj, _string, value)
|
||||
reloadAll()
|
||||
end
|
||||
|
||||
function onDestroy()
|
||||
if timerID and type(timerID) == 'object' then
|
||||
Timer.destroy(timerID)
|
||||
end
|
||||
end
|
134
src/core/ActiveInvestigatorCounter.ttslua
Normal file
134
src/core/ActiveInvestigatorCounter.ttslua
Normal file
@ -0,0 +1,134 @@
|
||||
DEBUG = false
|
||||
MIN_VALUE = 1
|
||||
MAX_VALUE = 4
|
||||
|
||||
function onload(saved_data)
|
||||
self.interactable = DEBUG
|
||||
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
|
132
src/core/AgendaDeck.ttslua
Normal file
132
src/core/AgendaDeck.ttslua
Normal file
@ -0,0 +1,132 @@
|
||||
MIN_VALUE = -99
|
||||
MAX_VALUE = 999
|
||||
|
||||
function onload(saved_data)
|
||||
light_mode = false
|
||||
val = 0
|
||||
|
||||
if saved_data ~= "" then
|
||||
local loaded_data = JSON.decode(saved_data)
|
||||
light_mode = loaded_data[1]
|
||||
val = loaded_data[2]
|
||||
end
|
||||
|
||||
createAll()
|
||||
end
|
||||
|
||||
function updateSave()
|
||||
local data_to_save = {light_mode, val}
|
||||
saved_data = JSON.encode(data_to_save)
|
||||
self.script_state = saved_data
|
||||
end
|
||||
|
||||
function createAll()
|
||||
s_color = {0.5, 0.5, 0.5, 95}
|
||||
|
||||
if light_mode then
|
||||
f_color = {1,1,1,95}
|
||||
else
|
||||
f_color = {0,0,0,100}
|
||||
end
|
||||
|
||||
|
||||
|
||||
self.createButton({
|
||||
label=tostring(val),
|
||||
click_function="add_subtract",
|
||||
function_owner=self,
|
||||
position={0,0.05,0},
|
||||
height=600,
|
||||
width=1000,
|
||||
alignment = 3,
|
||||
scale={x=1.5, y=1.5, z=1.5},
|
||||
font_size=600,
|
||||
font_color=f_color,
|
||||
color={0,0,0,0}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
if light_mode then
|
||||
lightButtonText = "[ Set dark ]"
|
||||
else
|
||||
lightButtonText = "[ Set light ]"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function removeAll()
|
||||
self.removeInput(0)
|
||||
self.removeInput(1)
|
||||
self.removeButton(0)
|
||||
self.removeButton(1)
|
||||
self.removeButton(2)
|
||||
end
|
||||
|
||||
function reloadAll()
|
||||
removeAll()
|
||||
createAll()
|
||||
|
||||
updateSave()
|
||||
end
|
||||
|
||||
function swap_fcolor(_obj, _color, alt_click)
|
||||
light_mode = not light_mode
|
||||
reloadAll()
|
||||
end
|
||||
|
||||
function swap_align(_obj, _color, alt_click)
|
||||
center_mode = not center_mode
|
||||
reloadAll()
|
||||
end
|
||||
|
||||
function editName(_obj, _string, value)
|
||||
self.setName(value)
|
||||
setTooltips()
|
||||
end
|
||||
|
||||
function add_subtract(_obj, _color, alt_click)
|
||||
mod = alt_click and -1 or 1
|
||||
new_value = math.min(math.max(val + mod, MIN_VALUE), MAX_VALUE)
|
||||
if val ~= new_value then
|
||||
val = new_value
|
||||
updateVal()
|
||||
updateSave()
|
||||
end
|
||||
end
|
||||
|
||||
function updateVal()
|
||||
|
||||
self.editButton({
|
||||
index = 0,
|
||||
label = tostring(val),
|
||||
|
||||
})
|
||||
end
|
||||
|
||||
function reset_val()
|
||||
val = 0
|
||||
updateVal()
|
||||
updateSave()
|
||||
end
|
||||
|
||||
function setTooltips()
|
||||
self.editInput({
|
||||
index = 0,
|
||||
value = self.getName(),
|
||||
tooltip = ttText
|
||||
})
|
||||
self.editButton({
|
||||
index = 0,
|
||||
value = tostring(val),
|
||||
tooltip = ttText
|
||||
})
|
||||
end
|
||||
|
||||
function null()
|
||||
end
|
||||
|
||||
function keepSample(_obj, _string, value)
|
||||
reloadAll()
|
||||
end
|
0
src/core/CustomDataHelper.ttslua
Normal file
0
src/core/CustomDataHelper.ttslua
Normal file
1979
src/core/DataHelper.ttslua
Normal file
1979
src/core/DataHelper.ttslua
Normal file
File diff suppressed because it is too large
Load Diff
768
src/core/Global.ttslua
Normal file
768
src/core/Global.ttslua
Normal file
@ -0,0 +1,768 @@
|
||||
--[[ Lua code. See documentation: http://berserk-games.com/knowledgebase/scripting/ --]]
|
||||
-- Card size used for autodealing --
|
||||
|
||||
-- global position constants
|
||||
ENCOUNTER_DECK_POS = {-3.8, 1, 5.7}
|
||||
ENCOUNTER_DECK_SPAWN_POS = {-3.8, 3, 5.7}
|
||||
ENCOUNTER_DECK_DISCARD_POSITION = {-3.8, 0.5, 10.5}
|
||||
g_cardWith=2.30;
|
||||
g_cardHeigth=3.40;
|
||||
|
||||
containerId = 'fea079'
|
||||
tokenDataId = '708279'
|
||||
|
||||
|
||||
maxSquid = 0
|
||||
|
||||
CACHE = {
|
||||
object = {},
|
||||
data = {}
|
||||
}
|
||||
|
||||
--[[ The OnLoad function. This is called after everything in the game save finishes loading.
|
||||
Most of your script code goes here. --]]
|
||||
function onload()
|
||||
--Player.White.changeColor('Yellow')
|
||||
tokenplayerone = {
|
||||
damageone = "http://cloud-3.steamusercontent.com/ugc/1758068501357115146/903D11AAE7BD5C254C8DC136E9202EE516289DEA/",
|
||||
damagethree = "http://cloud-3.steamusercontent.com/ugc/1758068501357113055/8A45F27B2838FED09DEFE492C9C40DD82781613A/",
|
||||
horrorone = "http://cloud-3.steamusercontent.com/ugc/1758068501357163535/6D9E0756503664D65BDB384656AC6D4BD713F5FC/",
|
||||
horrorthree = "http://cloud-3.steamusercontent.com/ugc/1758068501357162977/E5D453CC14394519E004B4F8703FC425A7AE3D6C/",
|
||||
resource = "http://cloud-3.steamusercontent.com/ugc/1758068501357192910/11DDDC7EF621320962FDCF3AE3211D5EDC3D1573/",
|
||||
resourcethree = "https://i.imgur.com/1GZsDTt.png",
|
||||
doom = "https://i.imgur.com/EoL7yaZ.png",
|
||||
clue = "http://cloud-3.steamusercontent.com/ugc/1758068501357164917/1D06F1DC4D6888B6F57124BD2AFE20D0B0DA15A8/"
|
||||
}
|
||||
|
||||
TOKEN_DATA = {
|
||||
clue = {image = tokenplayerone.clue, scale = {0.15, 0.15, 0.15}},
|
||||
resource = {image = tokenplayerone.resource, scale = {0.17, 0.17, 0.17}},
|
||||
doom = {image = tokenplayerone.doom, scale = {0.17, 0.17, 0.17}}
|
||||
}
|
||||
|
||||
getObjectFromGUID("6161b4").interactable=false
|
||||
getObjectFromGUID("721ba2").interactable=false
|
||||
getObjectFromGUID("9f334f").interactable=false
|
||||
getObjectFromGUID("23a43c").interactable=false
|
||||
getObjectFromGUID("5450cc").interactable=false
|
||||
getObjectFromGUID("463022").interactable=false
|
||||
getObjectFromGUID("9487a4").interactable=false
|
||||
getObjectFromGUID("91dd9b").interactable=false
|
||||
getObjectFromGUID("f182ee").interactable=false
|
||||
|
||||
end
|
||||
|
||||
function onObjectDrop(player, obj)
|
||||
-- local mat = getObjectFromGUID("dsbd0ff4")
|
||||
-- log(mat.positionToLocal(obj.getPosition()))
|
||||
end
|
||||
|
||||
function take_callback(object_spawned, mat)
|
||||
customObject = object_spawned.getCustomObject()
|
||||
local player = mat.getGUID();
|
||||
|
||||
local image = customObject.image
|
||||
|
||||
-- Update global stats
|
||||
if PULLS[image] == nil then
|
||||
PULLS[image] = 0
|
||||
end
|
||||
PULLS[image] = PULLS[image] + 1
|
||||
-- Update player stats
|
||||
if PLAYER_PULLS[player][image] == nil then
|
||||
PLAYER_PULLS[player][image] = 0
|
||||
end
|
||||
PLAYER_PULLS[player][image] = PLAYER_PULLS[player][image] + 1
|
||||
|
||||
end
|
||||
MAT_GUID_TO_COLOUR = {
|
||||
["8b081b"] = "White",
|
||||
-- player 2 conrad
|
||||
["bd0ff4"] = "Orange",
|
||||
-- player
|
||||
["383d8b"] = "Green",
|
||||
-- playur 4 olivia
|
||||
["0840d5"] = "Red"
|
||||
}
|
||||
|
||||
|
||||
PLAYER_PULLS = {
|
||||
-- player 1 max
|
||||
["8b081b"] = {},
|
||||
-- player 2 conrad
|
||||
["bd0ff4"] = {},
|
||||
-- player
|
||||
["383d8b"] = {},
|
||||
-- playur 4 olivia
|
||||
["0840d5"] = {}
|
||||
}
|
||||
|
||||
PULLS = {
|
||||
-- cultist
|
||||
["https://i.imgur.com/VzhJJaH.png"] = 0,
|
||||
-- skull
|
||||
["https://i.imgur.com/stbBxtx.png"] = 0,
|
||||
-- tablet
|
||||
["https://i.imgur.com/1plY463.png"] = 0,
|
||||
-- curse
|
||||
["http://cloud-3.steamusercontent.com/ugc/1655601092778636039/2A25BD38E8C44701D80DD96BF0121DA21843672E/"] = 0,
|
||||
-- tentacle
|
||||
["https://i.imgur.com/lns4fhz.png"] = 0,
|
||||
-- minus eight
|
||||
["https://i.imgur.com/9t3rPTQ.png"] = 0,
|
||||
-- minus seven
|
||||
["https://i.imgur.com/4WRD42n.png"] = 0,
|
||||
-- minus six
|
||||
["https://i.imgur.com/c9qdSzS.png"] = 0,
|
||||
-- minus five
|
||||
["https://i.imgur.com/3Ym1IeG.png"] = 0,
|
||||
-- minus four
|
||||
["https://i.imgur.com/qrgGQRD.png"] = 0,
|
||||
-- minus three
|
||||
["https://i.imgur.com/yfs8gHq.png"] = 0,
|
||||
-- minus two
|
||||
["https://i.imgur.com/bfTg2hb.png"] = 0,
|
||||
-- minus one
|
||||
["https://i.imgur.com/w3XbrCC.png"] = 0,
|
||||
-- zero
|
||||
["https://i.imgur.com/btEtVfd.png"] = 0,
|
||||
-- plus one
|
||||
["https://i.imgur.com/uIx8jbY.png"] = 0,
|
||||
-- elder thing
|
||||
["https://i.imgur.com/ttnspKt.png"] = 0,
|
||||
-- bless
|
||||
["http://cloud-3.steamusercontent.com/ugc/1655601092778627699/339FB716CB25CA6025C338F13AFDFD9AC6FA8356/"] = 0,
|
||||
-- elder sign
|
||||
["https://i.imgur.com/nEmqjmj.png"] = 0,
|
||||
}
|
||||
|
||||
IMAGE_TOKEN_MAP = {
|
||||
-- elder sign
|
||||
["https://i.imgur.com/nEmqjmj.png"] = "Elder Sign",
|
||||
-- plus one
|
||||
["https://i.imgur.com/uIx8jbY.png"] = "+1",
|
||||
-- zero
|
||||
["https://i.imgur.com/btEtVfd.png"] = "0",
|
||||
-- minus one
|
||||
["https://i.imgur.com/w3XbrCC.png"] = "-1",
|
||||
-- minus two
|
||||
["https://i.imgur.com/bfTg2hb.png"] = "-2",
|
||||
-- minus three
|
||||
["https://i.imgur.com/yfs8gHq.png"] = "-3",
|
||||
-- minus four
|
||||
["https://i.imgur.com/qrgGQRD.png"] = "-4",
|
||||
-- minus five
|
||||
["https://i.imgur.com/3Ym1IeG.png"] = "-5",
|
||||
-- minus six
|
||||
["https://i.imgur.com/c9qdSzS.png"] = "-6",
|
||||
-- minus seven
|
||||
["https://i.imgur.com/4WRD42n.png"] = "-7",
|
||||
-- minus eight
|
||||
["https://i.imgur.com/9t3rPTQ.png"] = "-8",
|
||||
-- skull
|
||||
["https://i.imgur.com/stbBxtx.png"] = "Skull",
|
||||
-- cultist
|
||||
["https://i.imgur.com/VzhJJaH.png"] = "Cultist",
|
||||
-- tablet
|
||||
["https://i.imgur.com/1plY463.png"] = "Tablet",
|
||||
-- elder thing
|
||||
["https://i.imgur.com/ttnspKt.png"] = "Elder Thing",
|
||||
-- tentacle
|
||||
["https://i.imgur.com/lns4fhz.png"] = "Auto-fail",
|
||||
-- bless
|
||||
["http://cloud-3.steamusercontent.com/ugc/1655601092778627699/339FB716CB25CA6025C338F13AFDFD9AC6FA8356/"] = "Bless",
|
||||
-- curse
|
||||
["http://cloud-3.steamusercontent.com/ugc/1655601092778636039/2A25BD38E8C44701D80DD96BF0121DA21843672E/"] = "Curse"
|
||||
}
|
||||
|
||||
function resetStats()
|
||||
for key,value in pairs(PULLS) do
|
||||
PULLS[key] = 0
|
||||
end
|
||||
for playerKey, playerValue in pairs(PLAYER_PULLS) do
|
||||
for key,value in pairs(PULLS) do
|
||||
PLAYER_PULLS[playerKey][key] = value
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
function getPlayerName(playerMatGuid)
|
||||
local playerColour = MAT_GUID_TO_COLOUR[playerMatGuid]
|
||||
if Player[playerColour].seated then
|
||||
return Player[playerColour].steam_name
|
||||
else
|
||||
return playerColour
|
||||
end
|
||||
end
|
||||
|
||||
function printStats()
|
||||
local squidKing = "Nobody"
|
||||
|
||||
|
||||
printToAll("\nOverall Game stats\n")
|
||||
printNonZeroTokenPairs(PULLS)
|
||||
printToAll("\nIndividual Stats\n")
|
||||
for playerMatGuid, countTable in pairs(PLAYER_PULLS) do
|
||||
local playerName = getPlayerName(playerMatGuid)
|
||||
printToAll(playerName .. " Stats", {r=255,g=0,b=0})
|
||||
printNonZeroTokenPairs(PLAYER_PULLS[playerMatGuid])
|
||||
playerSquidCount = PLAYER_PULLS[playerMatGuid]["https://i.imgur.com/lns4fhz.png"]
|
||||
if playerSquidCount ~= nil and playerSquidCount > maxSquid then
|
||||
squidKing = playerName
|
||||
maxSquid = playerSquidCount
|
||||
end
|
||||
end
|
||||
printToAll(squidKing .. " is an auto-fail magnet.", {r=255,g=0,b=0})
|
||||
end
|
||||
|
||||
function printNonZeroTokenPairs(theTable)
|
||||
for key,value in pairs(theTable) do
|
||||
if value ~= 0 then
|
||||
printToAll(IMAGE_TOKEN_MAP[key] .. '=' .. tostring(value))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Remove comments to enable autorotate cards on hands.
|
||||
-- function onObjectEnterScriptingZone(zone, object)
|
||||
-- Autorotate cards with right side up when entering hand.
|
||||
-- if zone.getGUID() == "c506bf" or -- white
|
||||
-- zone.getGUID() == "cbc751" then -- orange
|
||||
-- object.setRotationSmooth({0,270,0})
|
||||
-- elseif zone.getGUID() == "67ce9a" then -- green
|
||||
-- object.setRotationSmooth({0,0,0})
|
||||
-- elseif zone.getGUID() == "57c22c" then -- red
|
||||
-- object.setRotationSmooth({0,180,0})
|
||||
--end
|
||||
--end
|
||||
|
||||
function findInRadiusBy(pos, radius, filter, debug)
|
||||
local radius = (radius or 1)
|
||||
local objList = Physics.cast({
|
||||
origin = pos,
|
||||
direction = {0,1,0},
|
||||
type = 2,
|
||||
size = {radius, radius, radius},
|
||||
max_distance = 0,
|
||||
debug = (debug or false)
|
||||
})
|
||||
|
||||
local filteredList = {}
|
||||
for _, obj in ipairs(objList) do
|
||||
if filter == nil then
|
||||
table.insert(filteredList, obj.hit_object)
|
||||
elseif filter and filter(obj.hit_object) then
|
||||
table.insert(filteredList, obj.hit_object)
|
||||
end
|
||||
end
|
||||
return filteredList
|
||||
end
|
||||
|
||||
function dealCardsInRows(paramlist)
|
||||
local currPosition={};
|
||||
local numRow=1;
|
||||
local numCard=0;
|
||||
local invMultiplier=1;
|
||||
local allCardsDealed=0;
|
||||
if paramlist.inverse then
|
||||
invMultiplier=-1;
|
||||
end
|
||||
if paramlist.maxCardsDealed==nil then
|
||||
|
||||
allCardsDealed=0;
|
||||
paramlist.maxCardsDealed=paramlist.cardDeck.getQuantity()
|
||||
|
||||
elseif paramlist.maxCardsDealed>=paramlist.cardDeck.getQuantity() or paramlist.maxCardsDealed<=0 then
|
||||
|
||||
allCardsDealed=0;
|
||||
paramlist.maxCardsDealed=paramlist.cardDeck.getQuantity()
|
||||
|
||||
else
|
||||
|
||||
allCardsDealed=1;
|
||||
|
||||
end
|
||||
|
||||
if paramlist.mode=="x" then
|
||||
currPosition={paramlist.iniPosition[1]+(2*g_cardWith*invMultiplier*allCardsDealed),paramlist.iniPosition[2],paramlist.iniPosition[3]};
|
||||
|
||||
else
|
||||
currPosition={paramlist.iniPosition[1],paramlist.iniPosition[2],paramlist.iniPosition[3]+(2*g_cardWith*invMultiplier*allCardsDealed)};
|
||||
|
||||
end
|
||||
|
||||
for i = 1,paramlist.maxCardsDealed,1 do
|
||||
|
||||
paramlist.cardDeck.takeObject
|
||||
({
|
||||
position= currPosition,
|
||||
smooth= true
|
||||
});
|
||||
|
||||
numCard=numCard+1;
|
||||
if numCard>=paramlist.maxCardRow then
|
||||
|
||||
if paramlist.mode=="x" then
|
||||
currPosition={paramlist.iniPosition[1]+(2*g_cardWith*invMultiplier*allCardsDealed),paramlist.iniPosition[2],paramlist.iniPosition[3]};
|
||||
currPosition[3]=currPosition[3]-(numRow*g_cardHeigth*invMultiplier);
|
||||
else
|
||||
currPosition={paramlist.iniPosition[1],paramlist.iniPosition[2],paramlist.iniPosition[3]+(2*g_cardWith*invMultiplier*allCardsDealed)};
|
||||
currPosition[1]=currPosition[1]+(numRow*g_cardHeigth*invMultiplier);
|
||||
end
|
||||
numCard=0;
|
||||
numRow=numRow+1;
|
||||
|
||||
else
|
||||
if paramlist.mode=="x" then
|
||||
currPosition[1]=currPosition[1]+(g_cardWith*invMultiplier);
|
||||
else
|
||||
currPosition[3]=currPosition[3]+(g_cardWith*invMultiplier);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function isDeck(x)
|
||||
return x.tag == 'Deck'
|
||||
end
|
||||
|
||||
function isCardOrDeck(x)
|
||||
return x.tag == 'Card' or isDeck(x)
|
||||
end
|
||||
|
||||
function drawEncountercard(params) --[[ Parameter Table Position, Table Rotation]]
|
||||
local position = params[1]
|
||||
local rotation = params[2]
|
||||
local alwaysFaceUp = params[3]
|
||||
local faceUpRotation
|
||||
local card
|
||||
local items = findInRadiusBy(ENCOUNTER_DECK_POS, 4, isCardOrDeck)
|
||||
if #items > 0 then
|
||||
for i, v in ipairs(items) do
|
||||
if v.tag == 'Deck' then
|
||||
card = v.takeObject({index = 0})
|
||||
break
|
||||
end
|
||||
end
|
||||
-- we didn't find the deck so just pull the first thing we did find
|
||||
if card == nil then card = items[1] end
|
||||
actualEncounterCardDraw(card, params)
|
||||
return
|
||||
end
|
||||
-- nothing here, time to reshuffle
|
||||
reshuffleEncounterDeck(params)
|
||||
end
|
||||
|
||||
function actualEncounterCardDraw(card, params)
|
||||
local position = params[1]
|
||||
local rotation = params[2]
|
||||
local alwaysFaceUp = params[3]
|
||||
local faceUpRotation = 0
|
||||
if not alwaysFaceUp then
|
||||
if getObjectFromGUID(tokenDataId).call('checkHiddenCard', card.getName()) then
|
||||
faceUpRotation = 180
|
||||
end
|
||||
end
|
||||
card.setPositionSmooth(position, false, false)
|
||||
card.setRotationSmooth({0,rotation.y,faceUpRotation}, false, false)
|
||||
end
|
||||
|
||||
IS_RESHUFFLING = false
|
||||
function reshuffleEncounterDeck(params)
|
||||
-- finishes moving the deck back and draws a card
|
||||
local function move(deck)
|
||||
deck.setPositionSmooth(ENCOUNTER_DECK_SPAWN_POS, true, false)
|
||||
actualEncounterCardDraw(deck.takeObject({index=0}), params)
|
||||
Wait.time(function()
|
||||
IS_RESHUFFLING = false
|
||||
end, 1)
|
||||
end
|
||||
-- bail out if we're mid reshuffle
|
||||
if IS_RESHUFFLING then
|
||||
return
|
||||
end
|
||||
local discarded = findInRadiusBy(ENCOUNTER_DECK_DISCARD_POSITION, 4, isDeck)
|
||||
if #discarded > 0 then
|
||||
IS_RESHUFFLING = true
|
||||
local deck = discarded[1]
|
||||
if not deck.is_face_down then
|
||||
deck.flip()
|
||||
end
|
||||
deck.shuffle()
|
||||
Wait.time(|| move(deck), 0.3)
|
||||
else
|
||||
printToAll("couldn't find encounter discard pile to reshuffle", {1, 0, 0})
|
||||
end
|
||||
end
|
||||
|
||||
CHAOS_TOKENS = {}
|
||||
CHAOS_TOKENS_LAST_MAT = nil
|
||||
function putBackChaosTokens()
|
||||
local chaosbagposition = chaosbag.getPosition()
|
||||
for k, token in pairs(CHAOS_TOKENS) do
|
||||
if token ~= nil then
|
||||
chaosbag.putObject(token)
|
||||
token.setPosition({chaosbagposition[1],chaosbagposition[2]+0.5,chaosbagposition[3]})
|
||||
end
|
||||
end
|
||||
CHAOS_TOKENS = {}
|
||||
end
|
||||
|
||||
function drawChaostoken(params)
|
||||
local mat = params[1]
|
||||
local tokenOffset = params[2]
|
||||
local isRightClick = params[3]
|
||||
local isSameMat = (CHAOS_TOKENS_LAST_MAT == nil or CHAOS_TOKENS_LAST_MAT == mat)
|
||||
if not isSameMat then
|
||||
putBackChaosTokens()
|
||||
end
|
||||
CHAOS_TOKENS_LAST_MAT = mat
|
||||
-- if we have left clicked and have no tokens OR if we have right clicked
|
||||
if isRightClick or #CHAOS_TOKENS == 0 then
|
||||
local items = getObjectFromGUID("83ef06").getObjects()
|
||||
for i,v in ipairs(items) do
|
||||
if items[i].getDescription() == "Chaos Bag" then
|
||||
chaosbag = getObjectFromGUID(items[i].getGUID())
|
||||
break
|
||||
end
|
||||
end
|
||||
-- bail out if we have no tokens
|
||||
if #chaosbag.getObjects() == 0 then
|
||||
return
|
||||
end
|
||||
chaosbag.shuffle()
|
||||
-- add the token to the list, compute new position based on list length
|
||||
tokenOffset[1] = tokenOffset[1] + (0.17 * #CHAOS_TOKENS)
|
||||
local toPosition = mat.positionToWorld(tokenOffset)
|
||||
local token = chaosbag.takeObject({
|
||||
index = 0,
|
||||
position = toPosition,
|
||||
rotation = mat.getRotation(),
|
||||
callback_function = function(obj) take_callback(obj, mat) end
|
||||
})
|
||||
CHAOS_TOKENS[#CHAOS_TOKENS + 1] = token
|
||||
return
|
||||
else
|
||||
putBackChaosTokens()
|
||||
end
|
||||
end
|
||||
|
||||
function spawnToken(params)
|
||||
-- Position to spawn,
|
||||
-- rotation vector to apply
|
||||
-- translation vector to apply
|
||||
-- token type
|
||||
local position = params[1]
|
||||
local tokenType = params[2]
|
||||
local tokenData = TOKEN_DATA[tokenType]
|
||||
if tokenData == nil then
|
||||
error("no token data found for '" .. tokenType .. "'")
|
||||
end
|
||||
|
||||
local token = spawnObject({
|
||||
type = 'Custom_Token',
|
||||
position = position,
|
||||
rotation = {x=0, y=270, z=0}
|
||||
})
|
||||
token.setCustomObject({
|
||||
image = tokenData['image'],
|
||||
thickness = 0.3,
|
||||
merge_distance = 5.0,
|
||||
stackable = true,
|
||||
})
|
||||
token.use_snap_points=false
|
||||
token.scale(tokenData['scale'])
|
||||
return token
|
||||
end
|
||||
|
||||
function round(params) -- Parameter (int number, int numberDecimalPlaces)
|
||||
return tonumber(string.format("%." .. (params[2] or 0) .. "f", params[1]))
|
||||
end
|
||||
|
||||
function roundposition(params) -- Parameter (Table position)
|
||||
return {round({params[1], 2}),round({params[2], 2}),round({params[3], 2})}
|
||||
end
|
||||
|
||||
function isEqual(params) --Parameter (Table table1, Table table2) returns true if the tables are equal
|
||||
if params[1][1] == params[2][1] and params[1][2] == params[2][2] and params[1][3] == params[2][3] then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function isFaceup(params) --Object object
|
||||
if params.getRotation()[3] > -5 and params.getRotation()[3] < 5 then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--Difficulty selector script
|
||||
|
||||
function createSetupButtons(args)
|
||||
local data = getDataValue('modeData', args.key)
|
||||
if data ~= nil then
|
||||
local z = -0.15
|
||||
if data.easy ~= nil then
|
||||
args.object.createButton({
|
||||
label = 'Easy',
|
||||
click_function = 'easyClick',
|
||||
function_owner = args.object,
|
||||
position = {0, 0.1, z},
|
||||
rotation = {0, 0, 0},
|
||||
scale = {0.47, 1, 0.47},
|
||||
height = 200,
|
||||
width = 1150,
|
||||
font_size = 100,
|
||||
color = {0.87, 0.8, 0.70},
|
||||
font_color = {0, 0, 0}
|
||||
})
|
||||
z = z + 0.20
|
||||
end
|
||||
if data.normal ~= nil then
|
||||
args.object.createButton({
|
||||
label = 'Standard',
|
||||
click_function = 'normalClick',
|
||||
function_owner = args.object,
|
||||
position = {0, 0.1, z},
|
||||
rotation = {0, 0, 0},
|
||||
scale = {0.47, 1, 0.47},
|
||||
height = 200,
|
||||
width = 1150,
|
||||
font_size = 100,
|
||||
color = {0.87, 0.8, 0.70},
|
||||
font_color = {0, 0, 0}
|
||||
})
|
||||
z = z + 0.20
|
||||
end
|
||||
if data.hard ~= nil then
|
||||
args.object.createButton({
|
||||
label = 'Hard',
|
||||
click_function = 'hardClick',
|
||||
function_owner = args.object,
|
||||
position = {0, 0.1, z},
|
||||
rotation = {0, 0, 0},
|
||||
scale = {0.47, 1, 0.47},
|
||||
height = 200,
|
||||
width = 1150,
|
||||
font_size = 100,
|
||||
color = {0.87, 0.8, 0.70},
|
||||
font_color = {0, 0, 0}
|
||||
})
|
||||
z = z + 0.20
|
||||
end
|
||||
if data.expert ~= nil then
|
||||
args.object.createButton({
|
||||
label = 'Expert',
|
||||
click_function = 'expertClick',
|
||||
function_owner = args.object,
|
||||
position = {0, 0.1, z},
|
||||
rotation = {0, 0, 0},
|
||||
scale = {0.47, 1, 0.47},
|
||||
height = 200,
|
||||
width = 1150,
|
||||
font_size = 100,
|
||||
color = {0.87, 0.8, 0.70},
|
||||
font_color = {0, 0, 0}
|
||||
})
|
||||
z = z + 0.20
|
||||
end
|
||||
z = z + 0.10
|
||||
if data.standalone ~= nil then
|
||||
args.object.createButton({
|
||||
label = 'Standalone',
|
||||
click_function = 'standaloneClick',
|
||||
function_owner = args.object,
|
||||
position = {0, 0.1, z},
|
||||
rotation = {0, 0, 0},
|
||||
scale = {0.47, 1, 0.47},
|
||||
height = 200,
|
||||
width = 1150,
|
||||
font_size = 100,
|
||||
color = {0.87, 0.8, 0.70},
|
||||
font_color = {0, 0, 0}
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function fillContainer(args)
|
||||
local container = getObjectCache(containerId)
|
||||
|
||||
if container ~= nil then
|
||||
local data = getDataValue('modeData', args.key)
|
||||
if data == nil then return end
|
||||
|
||||
local value = data[args.mode]
|
||||
if value == nil or value.token == nil then return end
|
||||
|
||||
local pos = container.getPosition()
|
||||
if args.object ~= nil then
|
||||
pos = args.object.getPosition()
|
||||
end
|
||||
|
||||
cleanContainer(container)
|
||||
|
||||
for _, token in ipairs(value.token) do
|
||||
local obj = spawnToken_2(token, pos)
|
||||
if obj ~= nil then
|
||||
container.putObject(obj)
|
||||
end
|
||||
end
|
||||
|
||||
if value.append ~= nil then
|
||||
for _, token in ipairs(value.append) do
|
||||
local obj = spawnToken_2(token, pos)
|
||||
if obj ~= nil then
|
||||
container.putObject(obj)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if value.random then
|
||||
local n = #value.random
|
||||
if n > 0 then
|
||||
for _, token in ipairs(value.random[getRandomCount(n)]) do
|
||||
local obj = spawnToken_2(token, pos)
|
||||
if obj ~= nil then
|
||||
container.putObject(obj)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if value.message then
|
||||
broadcastToAll(value.message)
|
||||
end
|
||||
if value.warning then
|
||||
broadcastToAll(value.warning, { 1, 0.5, 0.5 })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function spawnToken_2(id, pos)
|
||||
local url = getImageUrl(id)
|
||||
if url ~= '' then
|
||||
local obj = spawnObject({
|
||||
type = 'Custom_Tile',
|
||||
position = {pos.x, pos.y + 3, pos.z},
|
||||
rotation = {x = 0, y = 260, z = 0}
|
||||
})
|
||||
obj.setCustomObject({
|
||||
type = 2,
|
||||
image = url,
|
||||
thickness = 0.10,
|
||||
})
|
||||
obj.scale {0.81, 1, 0.81}
|
||||
obj.setName(getTokenName({ url=url }))
|
||||
return obj
|
||||
end
|
||||
end
|
||||
|
||||
function getTokenName(params)
|
||||
local name = IMAGE_TOKEN_MAP[params.url]
|
||||
if name == nil then name = "" end
|
||||
return name
|
||||
end
|
||||
|
||||
function getImageUrl(id)
|
||||
if id == 'p1' then return 'https://i.imgur.com/uIx8jbY.png' end
|
||||
if id == '0' then return 'https://i.imgur.com/btEtVfd.png' end
|
||||
if id == 'm1' then return 'https://i.imgur.com/w3XbrCC.png' end
|
||||
if id == 'm2' then return 'https://i.imgur.com/bfTg2hb.png' end
|
||||
if id == 'm3' then return 'https://i.imgur.com/yfs8gHq.png' end
|
||||
if id == 'm4' then return 'https://i.imgur.com/qrgGQRD.png' end
|
||||
if id == 'm5' then return 'https://i.imgur.com/3Ym1IeG.png' end
|
||||
if id == 'm6' then return 'https://i.imgur.com/c9qdSzS.png' end
|
||||
if id == 'm7' then return 'https://i.imgur.com/4WRD42n.png' end
|
||||
if id == 'm8' then return 'https://i.imgur.com/9t3rPTQ.png' end
|
||||
if id == 'skull' then return 'https://i.imgur.com/stbBxtx.png' end
|
||||
if id == 'cultist' then return 'https://i.imgur.com/VzhJJaH.png' end
|
||||
if id == 'tablet' then return 'https://i.imgur.com/1plY463.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 == 'blue' then return 'https://i.imgur.com/nEmqjmj.png' end
|
||||
return ''
|
||||
end
|
||||
|
||||
function cleanContainer(container)
|
||||
for _, item in ipairs(container.getObjects()) do
|
||||
destroyObject(container.takeObject({}))
|
||||
end
|
||||
end
|
||||
|
||||
function getObjectsInZone(zoneId)
|
||||
local zoneObject = getObjectCache(zoneId)
|
||||
|
||||
if zoneObject == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local objectsInZone = zoneObject.getObjects()
|
||||
local objectsFound = {}
|
||||
|
||||
for i = 1, #objectsInZone do
|
||||
local object = objectsInZone[i]
|
||||
if object.tag == 'Bag' then
|
||||
table.insert(objectsFound, object.guid)
|
||||
end
|
||||
end
|
||||
|
||||
if #objectsFound > 0 then
|
||||
return objectsFound
|
||||
end
|
||||
end
|
||||
|
||||
function getObjectCache(id)
|
||||
if CACHE.object[id] == nil then
|
||||
CACHE.object[id] = getObjectFromGUID(id)
|
||||
end
|
||||
return CACHE.object[id]
|
||||
end
|
||||
|
||||
function getDataTable(storage)
|
||||
if CACHE.data[storage] == nil then
|
||||
local obj = getObjectCache(tokenDataId)
|
||||
if obj ~= nil then
|
||||
CACHE.data[storage] = obj.getTable(storage)
|
||||
end
|
||||
end
|
||||
return CACHE.data[storage]
|
||||
end
|
||||
|
||||
function getDataValue(storage, key)
|
||||
local data = getDataTable(storage)
|
||||
if data ~= nil then
|
||||
local value = data[key]
|
||||
if value ~= nil then
|
||||
local res = {}
|
||||
for m, v in pairs(value) do
|
||||
res[m] = v
|
||||
if res[m].parent ~= nil then
|
||||
local parentData = getDataValue(storage, res[m].parent)
|
||||
if parentData ~= nil and parentData[m] ~= nil and parentData[m].token ~= nil then
|
||||
res[m].token = parentData[m].token
|
||||
end
|
||||
res[m].parent = nil
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function getRandomCount(to)
|
||||
updateRandomSeed()
|
||||
return math.random(1, to)
|
||||
end
|
||||
|
||||
function updateRandomSeed()
|
||||
local chance = math.random(1,10)
|
||||
if chance == 1 then
|
||||
math.randomseed(os.time())
|
||||
end
|
||||
end
|
161
src/core/MasterClueCounter.ttslua
Normal file
161
src/core/MasterClueCounter.ttslua
Normal file
@ -0,0 +1,161 @@
|
||||
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
|
||||
p1ClueCounter = getObjectFromGUID("37be78")
|
||||
p2ClueCounter = getObjectFromGUID("1769ed")
|
||||
p3ClueCounter = getObjectFromGUID("032300")
|
||||
p4ClueCounter = getObjectFromGUID("d86b7c")
|
||||
|
||||
timerID = self.getGUID()..math.random(9999999999999)
|
||||
Timer.create({
|
||||
identifier=timerID,
|
||||
function_name="totalCounters", function_owner=self,
|
||||
repetitions=0, delay=1
|
||||
})
|
||||
createAll()
|
||||
end
|
||||
|
||||
function loadPlayerCounters()
|
||||
p1ClueCounter = getObjectFromGUID("37be78")
|
||||
p2ClueCounter = getObjectFromGUID("1769ed")
|
||||
p3ClueCounter = getObjectFromGUID("032300")
|
||||
p4ClueCounter = getObjectFromGUID("d86b7c")
|
||||
end
|
||||
|
||||
|
||||
function totalCounters()
|
||||
if p1ClueCounter == nil or p2ClueCounter == nil or p3ClueCounter == nil or p4ClueCounter == nil then
|
||||
loadPlayerCounters()
|
||||
end
|
||||
local p1ClueCount = p1ClueCounter.getVar("exposedValue")
|
||||
local p2ClueCount = p2ClueCounter.getVar("exposedValue")
|
||||
local p3ClueCount = p3ClueCounter.getVar("exposedValue")
|
||||
local p4ClueCount = p4ClueCounter.getVar("exposedValue")
|
||||
val = tonumber(p1ClueCount) + tonumber(p2ClueCount) + tonumber(p3ClueCount) + tonumber(p4ClueCount)
|
||||
updateVal()
|
||||
updateSave()
|
||||
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="removeAllPlayerClues",
|
||||
function_owner=self,
|
||||
position={0,0.05,0},
|
||||
height=600,
|
||||
width=1000,
|
||||
alignment = 3,
|
||||
tooltip = "Click button to remove all clues from all investigators",
|
||||
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 removeAllPlayerClues()
|
||||
p1ClueCounter.call("removeAllClues")
|
||||
p2ClueCounter.call("removeAllClues")
|
||||
p3ClueCounter.call("removeAllClues")
|
||||
p4ClueCounter.call("removeAllClues")
|
||||
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 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 = "Click button to remove all clues from all investigators"
|
||||
})
|
||||
self.editButton({
|
||||
index = 0,
|
||||
value = tostring(val),
|
||||
|
||||
})
|
||||
end
|
||||
|
||||
function null()
|
||||
end
|
||||
|
||||
function keepSample(_obj, _string, value)
|
||||
reloadAll()
|
||||
end
|
||||
|
||||
function onDestroy()
|
||||
if timerID and type(timerID) == 'object' then
|
||||
Timer.destroy(timerID)
|
||||
end
|
||||
end
|
214
src/core/PlayArea.ttslua
Normal file
214
src/core/PlayArea.ttslua
Normal file
@ -0,0 +1,214 @@
|
||||
-- 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
|
||||
|
||||
-- TODO get the log function from global instead
|
||||
-- log = Global.call('getLogFunction', this)
|
||||
function getLogFunction(object)
|
||||
return function (message)
|
||||
if DEBUG then
|
||||
print(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
log = getLogFunction(self)
|
||||
|
||||
function onload(save_state)
|
||||
self.interactable = DEBUG
|
||||
local dataHelper = getObjectFromGUID('708279')
|
||||
LOCATIONS = dataHelper.getTable('LOCATIONS_DATA')
|
||||
|
||||
TOKEN_PLAYER_ONE = Global.getTable('tokenplayerone')
|
||||
COUNTER = getObjectFromGUID('f182ee')
|
||||
log('attempting to load state: ' .. save_state)
|
||||
if save_state ~= '' then
|
||||
SPAWNED_LOCATION_GUIDS = JSON.decode(save_state)
|
||||
end
|
||||
|
||||
COLLISION_ENABLED = true
|
||||
end
|
||||
|
||||
function onSave()
|
||||
local spawned_locations = JSON.encode(SPAWNED_LOCATION_GUIDS)
|
||||
self.script_state = spawned_locations
|
||||
end
|
||||
|
||||
--[[
|
||||
records locations we have spawned clues for, we write this to the save
|
||||
file onsave() so we don't spawn clues again after a load
|
||||
]]
|
||||
SPAWNED_LOCATION_GUIDS = {}
|
||||
|
||||
function isAlreadySpawned(object)
|
||||
return SPAWNED_LOCATION_GUIDS[object.getGUID()] ~= nil
|
||||
end
|
||||
|
||||
function markSpawned(object)
|
||||
SPAWNED_LOCATION_GUIDS[object.getGUID()] = 1
|
||||
end
|
||||
|
||||
function buildKey(object)
|
||||
return object.getName() .. '_' .. object.getGUID()
|
||||
end
|
||||
|
||||
-- try the compound key then the name alone as default
|
||||
function getLocation(object)
|
||||
return LOCATIONS[buildKey(object)] or LOCATIONS[object.getName()]
|
||||
end
|
||||
|
||||
function isLocationWithClues(object)
|
||||
return getLocation(object) ~= nil
|
||||
end
|
||||
|
||||
--[[
|
||||
Return the number of clues to spawn on this location
|
||||
]]
|
||||
function getClueCount(object, isFaceDown, playerCount)
|
||||
if not isLocationWithClues(object) then
|
||||
error('attempted to get clue for unexpected object: ' .. object.getName())
|
||||
end
|
||||
local details = getLocation(object)
|
||||
log(object.getName() .. ' : ' .. details['type'] .. ' : ' .. details['value'] .. ' : ' .. details['clueSide'])
|
||||
if ((isFaceDown and details['clueSide'] == 'back')
|
||||
or (not isFaceDown and details['clueSide'] == 'front')) then
|
||||
if details['type'] == 'fixed' then
|
||||
return details['value']
|
||||
elseif details['type'] == 'perPlayer' then
|
||||
return details['value'] * playerCount
|
||||
end
|
||||
error('unexpected location type: ' .. details['type'])
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
function spawnToken(position, number)
|
||||
local obj_parameters = {
|
||||
position = position,
|
||||
rotation = {3.87674022, -90, 0.239081308}
|
||||
}
|
||||
local custom = {
|
||||
thickness = 0.1,
|
||||
stackable = true
|
||||
}
|
||||
|
||||
if number == '1' or number == '2' then
|
||||
obj_parameters.type = 'Custom_Token'
|
||||
custom.merge_distance = 5.0
|
||||
local token = spawnObject(obj_parameters)
|
||||
if number == '1' then
|
||||
custom.image = TOKEN_PLAYER_ONE.damageone
|
||||
token.setCustomObject(custom)
|
||||
token.scale {0.17, 1, 0.17}
|
||||
return token
|
||||
end
|
||||
|
||||
if number == '2' then
|
||||
custom.image = TOKEN_PLAYER_ONE.damagethree
|
||||
token.setCustomObject(custom)
|
||||
token.scale {0.18, 1, 0.18}
|
||||
return token
|
||||
end
|
||||
end
|
||||
|
||||
if number == '3' or number == '4' then
|
||||
obj_parameters.type = 'Custom_Tile'
|
||||
custom.type = 2
|
||||
local token = spawnObject(obj_parameters)
|
||||
if number == '3' then
|
||||
custom.image = TOKEN_PLAYER_ONE.clue
|
||||
custom.image_bottom = TOKEN_PLAYER_ONE.doom
|
||||
token.setCustomObject(custom)
|
||||
token.scale {0.25, 1, 0.25}
|
||||
token.use_snap_points=false
|
||||
return token
|
||||
end
|
||||
|
||||
if number == '4' then
|
||||
custom.image = TOKEN_PLAYER_ONE.doom
|
||||
custom.image_bottom = TOKEN_PLAYER_ONE.clue
|
||||
token.setCustomObject(custom)
|
||||
token.scale {0.25, 1, 0.25}
|
||||
token.use_snap_points=false
|
||||
return token
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
function spawnCluesAtLocation(clueCount, collision_info)
|
||||
local object = collision_info.collision_object
|
||||
if isAlreadySpawned(object) then
|
||||
error('tried to spawn clue for already spawned location:' .. object.getName())
|
||||
end
|
||||
|
||||
local obj_parameters = {}
|
||||
obj_parameters.type = 'Custom_Token'
|
||||
obj_parameters.position = {
|
||||
object.getPosition()[1],
|
||||
object.getPosition()[2] + 1,
|
||||
object.getPosition()[3]
|
||||
}
|
||||
|
||||
log('spawning clues for ' .. object.getName() .. '_' .. object.getGUID())
|
||||
local playerCount = COUNTER.getVar('val')
|
||||
log('player count is ' .. playerCount .. ', clue count is ' .. clueCount)
|
||||
-- mark this location as spawned, can't happen again
|
||||
markSpawned(object)
|
||||
i = 0
|
||||
while i < clueCount do
|
||||
if i < 4 then
|
||||
obj_parameters.position = {
|
||||
collision_info.collision_object.getPosition()[1] + 0.3,
|
||||
collision_info.collision_object.getPosition()[2] + 0.2,
|
||||
collision_info.collision_object.getPosition()[3] - 0.8 + (0.55 * i)
|
||||
}
|
||||
elseif i < 8 then
|
||||
obj_parameters.position = {
|
||||
collision_info.collision_object.getPosition()[1] + 0.85,
|
||||
collision_info.collision_object.getPosition()[2] + 0.2,
|
||||
collision_info.collision_object.getPosition()[3] - 3 + (0.55 * i)
|
||||
}
|
||||
else
|
||||
obj_parameters.position = {
|
||||
collision_info.collision_object.getPosition()[1] + 0.575,
|
||||
collision_info.collision_object.getPosition()[2] + 0.4,
|
||||
collision_info.collision_object.getPosition()[3] - 5.2 + (0.55 * i)}
|
||||
end
|
||||
spawnToken(obj_parameters.position, '3')
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function updateLocations(args)
|
||||
local custom_data_helper = getObjectFromGUID(args[1])
|
||||
data_locations = custom_data_helper.getTable("LOCATIONS_DATA")
|
||||
for k, v in pairs(data_locations) do
|
||||
LOCATIONS[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
function onCollisionEnter(collision_info)
|
||||
-- short circuit all collision stuff until we've loaded state
|
||||
if not COLLISION_ENABLED then
|
||||
return
|
||||
end
|
||||
|
||||
-- check if we should spawn clues here
|
||||
local object = collision_info.collision_object
|
||||
if isLocationWithClues(object) and not isAlreadySpawned(object) then
|
||||
-- this isn't an either/or as down/up here means at a relatively low angle
|
||||
-- local isFaceUp = not object.is_face_down
|
||||
-- local isFaceDown = (object.getRotation()[3] > 160 and object.getRotation()[3] < 200)
|
||||
local playerCount = COUNTER.getVar('val')
|
||||
local clueCount = getClueCount(object, object.is_face_down, playerCount)
|
||||
if clueCount > 0 then
|
||||
spawnCluesAtLocation(clueCount, collision_info)
|
||||
end
|
||||
end
|
||||
end
|
199
src/core/PlayAreaSelector.ttslua
Normal file
199
src/core/PlayAreaSelector.ttslua
Normal file
@ -0,0 +1,199 @@
|
||||
|
||||
|
||||
function onSave()
|
||||
saved_data = JSON.encode({tid=tableImageData, cd=checkData})
|
||||
--saved_data = ""
|
||||
return saved_data
|
||||
end
|
||||
|
||||
function onload(saved_data)
|
||||
--Loads the tracking for if the game has started yet
|
||||
if saved_data ~= "" then
|
||||
local loaded_data = JSON.decode(saved_data)
|
||||
tableImageData = loaded_data.tid
|
||||
checkData = loaded_data.cd
|
||||
else
|
||||
tableImageData = {}
|
||||
checkData = {move=false, scale=false}
|
||||
end
|
||||
|
||||
--Disables interactable status of objects with GUID in list
|
||||
for _, guid in ipairs(ref_noninteractable) do
|
||||
local obj = getObjectFromGUID(guid)
|
||||
if obj then obj.interactable = false end
|
||||
end
|
||||
|
||||
|
||||
|
||||
obj_surface = getObjectFromGUID("721ba2")
|
||||
|
||||
|
||||
controlActive = false
|
||||
createOpenCloseButton()
|
||||
end
|
||||
|
||||
|
||||
|
||||
--Activation/deactivation of control panel
|
||||
|
||||
|
||||
|
||||
--Activated by clicking on
|
||||
function click_toggleControl(_, color)
|
||||
if permissionCheck(color) then
|
||||
if not controlActive then
|
||||
--Activate control panel
|
||||
controlActive = true
|
||||
self.clearButtons()
|
||||
createOpenCloseButton()
|
||||
createSurfaceInput()
|
||||
createSurfaceButtons()
|
||||
|
||||
else
|
||||
--Deactivate control panel
|
||||
controlActive = false
|
||||
self.clearButtons()
|
||||
self.clearInputs()
|
||||
createOpenCloseButton()
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
--Table surface control
|
||||
|
||||
|
||||
|
||||
--Changes table surface
|
||||
function click_applySurface(_, color)
|
||||
if permissionCheck(color) then
|
||||
updateSurface()
|
||||
broadcastToAll("New Playmat Image Applied", {0.2,0.9,0.2})
|
||||
end
|
||||
end
|
||||
|
||||
--Updates surface from the values in the input field
|
||||
function updateSurface()
|
||||
local customInfo = obj_surface.getCustomObject()
|
||||
customInfo.image = self.getInputs()[1].value
|
||||
obj_surface.setCustomObject(customInfo)
|
||||
obj_surface = obj_surface.reload()
|
||||
end
|
||||
|
||||
|
||||
|
||||
--Information gathering
|
||||
|
||||
|
||||
|
||||
--Checks if a color is promoted or host
|
||||
function permissionCheck(color)
|
||||
if Player[color].host==true or Player[color].promoted==true then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--Locates a string saved within memory file
|
||||
function findInImageDataIndex(...)
|
||||
for _, str in ipairs({...}) do
|
||||
for i, v in ipairs(tableImageData) do
|
||||
if v.url == str or v.name == str then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--Round number (num) to the Nth decimal (dec)
|
||||
function round(num, dec)
|
||||
local mult = 10^(dec or 0)
|
||||
return math.floor(num * mult + 0.5) / mult
|
||||
end
|
||||
|
||||
--Locates a button with a helper function
|
||||
function findButton(obj, func)
|
||||
if func==nil then error("No func supplied to findButton") end
|
||||
for _, v in ipairs(obj.getButtons()) do
|
||||
if func(v) then
|
||||
return v
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
--Creation of buttons/inputs
|
||||
|
||||
|
||||
|
||||
function createOpenCloseButton()
|
||||
local tooltip = "Open Playmat Panel"
|
||||
if controlActive then
|
||||
tooltip = "Close Playmat Panel"
|
||||
end
|
||||
self.createButton({
|
||||
click_function="click_toggleControl", function_owner=self,
|
||||
position={0,0,0}, rotation={-45,0,0}, height=1500, width=1500,
|
||||
color={1,1,1,0}, tooltip=tooltip
|
||||
})
|
||||
end
|
||||
|
||||
function createSurfaceInput()
|
||||
local currentURL = obj_surface.getCustomObject().diffuse
|
||||
local nickname = ""
|
||||
if findInImageDataIndex(currentURL) ~= nil then
|
||||
nickname = tableImageData[findInImageDataIndex(currentURL)].name
|
||||
end
|
||||
|
||||
self.createInput({
|
||||
label="URL", input_function="none", function_owner=self,
|
||||
alignment=3, position={0,0.15,3}, height=224, width=4000,
|
||||
font_size=200, tooltip="Enter URL for playmat image",
|
||||
value=currentURL
|
||||
})
|
||||
end
|
||||
|
||||
function createSurfaceButtons()
|
||||
--Label
|
||||
self.createButton({
|
||||
label="Playmat Image Swapper", click_function="none",
|
||||
position={0,0.15,2.2}, height=0, width=0, font_size=300, font_color={1,1,1}
|
||||
})
|
||||
--Functional
|
||||
self.createButton({
|
||||
label="Apply Image\nTo Playmat", click_function="click_applySurface",
|
||||
function_owner=self, tooltip="Apply URL as playmat image",
|
||||
position={0,0.15,4}, height=440, width=1400, font_size=200,
|
||||
})
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
--Data tables
|
||||
|
||||
|
||||
|
||||
|
||||
ref_noninteractable = {
|
||||
"afc863","c8edca","393bf7","12c65e","f938a2","9f95fd","35b95f",
|
||||
"5af8f2","4ee1f2","bd69bd"
|
||||
}
|
||||
|
||||
ref_playerColor = {
|
||||
"White", "Brown", "Red", "Orange", "Yellow",
|
||||
"Green", "Teal", "Blue", "Purple", "Pink", "Black"
|
||||
}
|
||||
|
||||
--Dummy function, absorbs unwanted triggers
|
||||
function none() end
|
240
src/playercards/AllCardsBag.ttslua
Normal file
240
src/playercards/AllCardsBag.ttslua
Normal file
@ -0,0 +1,240 @@
|
||||
|
||||
local cardIdIndex = { }
|
||||
local classAndLevelIndex = { }
|
||||
local basicWeaknessList = { }
|
||||
|
||||
local indexingDone = false
|
||||
local allowRemoval = false
|
||||
|
||||
function onLoad()
|
||||
self.addContextMenuItem("Rebuild Index", startIndexBuild)
|
||||
math.randomseed(os.time())
|
||||
Wait.frames(startIndexBuild, 30)
|
||||
end
|
||||
|
||||
-- Called by Hotfix bags when they load. If we are still loading indexes, then
|
||||
-- the all cards and hotfix bags are being loaded together, and we can ignore
|
||||
-- this call as the hotfix will be included in the initial indexing. If it is
|
||||
-- called once indexing is complete it means the hotfix bag has been added
|
||||
-- later, and we should rebuild the index to integrate the hotfix bag.
|
||||
function rebuildIndexForHotfix()
|
||||
if (indexingDone) then
|
||||
startIndexBuild()
|
||||
end
|
||||
end
|
||||
|
||||
-- Resets all current bag indexes
|
||||
function clearIndexes()
|
||||
indexingDone = false
|
||||
cardIdIndex = { }
|
||||
classAndLevelIndex = { }
|
||||
classAndLevelIndex["Guardian-upgrade"] = { }
|
||||
classAndLevelIndex["Seeker-upgrade"] = { }
|
||||
classAndLevelIndex["Mystic-upgrade"] = { }
|
||||
classAndLevelIndex["Survivor-upgrade"] = { }
|
||||
classAndLevelIndex["Rogue-upgrade"] = { }
|
||||
classAndLevelIndex["Neutral-upgrade"] = { }
|
||||
classAndLevelIndex["Guardian-level0"] = { }
|
||||
classAndLevelIndex["Seeker-level0"] = { }
|
||||
classAndLevelIndex["Mystic-level0"] = { }
|
||||
classAndLevelIndex["Survivor-level0"] = { }
|
||||
classAndLevelIndex["Rogue-level0"] = { }
|
||||
classAndLevelIndex["Neutral-level0"] = { }
|
||||
basicWeaknessList = { }
|
||||
end
|
||||
|
||||
-- Clears the bag indexes and starts the coroutine to rebuild the indexes
|
||||
function startIndexBuild(playerColor)
|
||||
clearIndexes()
|
||||
startLuaCoroutine(self, "buildIndex")
|
||||
end
|
||||
|
||||
function onObjectLeaveContainer(container, object)
|
||||
if (container == self and not allowRemoval) then
|
||||
broadcastToAll(
|
||||
"Removing cards from the All Player Cards bag may break some functions. Please replace the card.",
|
||||
{0.9, 0.2, 0.2}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
-- Debug option to suppress the warning when cards are removed from the bag
|
||||
function setAllowCardRemoval()
|
||||
allowRemoval = true
|
||||
end
|
||||
|
||||
-- Create the card indexes by iterating all cards in the bag, parsing their
|
||||
-- metadata, and creating the keyed lookup tables for the cards. This is a
|
||||
-- coroutine which will spread the workload by processing 20 cards before
|
||||
-- yielding. Based on the current count of cards this will require
|
||||
-- approximately 60 frames to complete.
|
||||
function buildIndex()
|
||||
indexingDone = false
|
||||
if (self.getData().ContainedObjects == nil) then
|
||||
return 1
|
||||
end
|
||||
for i, cardData in ipairs(self.getData().ContainedObjects) do
|
||||
local cardMetadata = JSON.decode(cardData.GMNotes)
|
||||
if (cardMetadata ~= nil) then
|
||||
addCardToIndex(cardData, cardMetadata)
|
||||
end
|
||||
if (i % 20 == 0) then
|
||||
coroutine.yield(0)
|
||||
end
|
||||
end
|
||||
local hotfixBags = getObjectsWithTag("AllCardsHotfix")
|
||||
for _, hotfixBag in ipairs(hotfixBags) do
|
||||
if (#hotfixBag.getObjects() > 0) then
|
||||
for i, cardData in ipairs(hotfixBag.getData().ContainedObjects) do
|
||||
local cardMetadata = JSON.decode(cardData.GMNotes)
|
||||
if (cardMetadata ~= nil) then
|
||||
addCardToIndex(cardData, cardMetadata)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
buildSupplementalIndexes()
|
||||
indexingDone = true
|
||||
return 1
|
||||
end
|
||||
|
||||
-- Adds a card to any indexes it should be a part of, based on its metadata.
|
||||
-- Param cardData: TTS object data for the card
|
||||
-- Param cardMetadata: SCED metadata for the card
|
||||
function addCardToIndex(cardData, cardMetadata)
|
||||
cardIdIndex[cardMetadata.id] = { data = cardData, metadata = cardMetadata }
|
||||
if (cardMetadata.alternate_ids ~= nil) then
|
||||
for _, alternateId in ipairs(cardMetadata.alternate_ids) do
|
||||
cardIdIndex[alternateId] = { data = cardData, metadata = cardMetadata }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function buildSupplementalIndexes()
|
||||
for cardId, card in pairs(cardIdIndex) do
|
||||
local cardData = card.data
|
||||
local cardMetadata = card.metadata
|
||||
-- Add card to the basic weakness list, if appropriate. Some weaknesses have
|
||||
-- multiple copies, and are added multiple times
|
||||
if (cardMetadata.weakness and cardMetadata.basicWeaknessCount ~= nil) then
|
||||
for i = 1, cardMetadata.basicWeaknessCount do
|
||||
table.insert(basicWeaknessList, cardMetadata.id)
|
||||
end
|
||||
end
|
||||
|
||||
-- Add the card to the appropriate class and level indexes
|
||||
local isGuardian = false
|
||||
local isSeeker = false
|
||||
local isMystic = false
|
||||
local isRogue = false
|
||||
local isSurvivor = false
|
||||
local isNeutral = false
|
||||
local upgradeKey
|
||||
if (cardMetadata.class ~= nil and cardMetadata.level ~= nil) then
|
||||
isGuardian = string.match(cardMetadata.class, "Guardian")
|
||||
isSeeker = string.match(cardMetadata.class, "Seeker")
|
||||
isMystic = string.match(cardMetadata.class, "Mystic")
|
||||
isRogue = string.match(cardMetadata.class, "Rogue")
|
||||
isSurvivor = string.match(cardMetadata.class, "Survivor")
|
||||
isNeutral = string.match(cardMetadata.class, "Neutral")
|
||||
if (cardMetadata.level > 0) then
|
||||
upgradeKey = "-upgrade"
|
||||
else
|
||||
upgradeKey = "-level0"
|
||||
end
|
||||
if (isGuardian) then
|
||||
table.insert(classAndLevelIndex["Guardian"..upgradeKey], cardMetadata.id)
|
||||
end
|
||||
if (isSeeker) then
|
||||
table.insert(classAndLevelIndex["Seeker"..upgradeKey], cardMetadata.id)
|
||||
end
|
||||
if (isMystic) then
|
||||
table.insert(classAndLevelIndex["Mystic"..upgradeKey], cardMetadata.id)
|
||||
end
|
||||
if (isRogue) then
|
||||
table.insert(classAndLevelIndex["Rogue"..upgradeKey], cardMetadata.id)
|
||||
end
|
||||
if (isSurvivor) then
|
||||
table.insert(classAndLevelIndex["Survivor"..upgradeKey], cardMetadata.id)
|
||||
end
|
||||
if (isNeutral) then
|
||||
table.insert(classAndLevelIndex["Neutral"..upgradeKey], cardMetadata.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, indexTable in pairs(classAndLevelIndex) do
|
||||
table.sort(indexTable, cardComparator)
|
||||
end
|
||||
end
|
||||
|
||||
-- Comparison function used to sort the class card bag indexes. Sorts by card
|
||||
-- level, then name, then subname.
|
||||
function cardComparator(id1, id2)
|
||||
local card1 = cardIdIndex[id1]
|
||||
local card2 = cardIdIndex[id2]
|
||||
if (card1.metadata.level ~= card2.metadata.level) then
|
||||
return card1.metadata.level < card2.metadata.level
|
||||
end
|
||||
if (card1.data.Nickname ~= card2.data.Nickname) then
|
||||
return card1.data.Nickname < card2.data.Nickname
|
||||
end
|
||||
return card1.data.Description < card2.data.Description
|
||||
end
|
||||
|
||||
function isIndexReady()
|
||||
return indexingDone
|
||||
end
|
||||
|
||||
-- Returns a specific card from the bag, based on ArkhamDB ID
|
||||
-- Params table:
|
||||
-- id: String ID of the card to retrieve
|
||||
-- Return: If the indexes are still being constructed, an empty table is
|
||||
-- returned. Otherwise, a single table with the following fields
|
||||
-- cardData: TTS object data, suitable for spawning the card
|
||||
-- cardMetadata: Table of parsed metadata
|
||||
function getCardById(params)
|
||||
if (not indexingDone) then
|
||||
broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2})
|
||||
return { }
|
||||
end
|
||||
return cardIdIndex[params.id]
|
||||
end
|
||||
|
||||
-- Returns a list of cards from the bag matching a class and level (0 or upgraded)
|
||||
-- Params table:
|
||||
-- class: String class to retrieve ("Guardian", "Seeker", etc)
|
||||
-- isUpgraded: true for upgraded cards (Level 1-5), false for Level 0
|
||||
-- Return: If the indexes are still being constructed, returns an empty table.
|
||||
-- Otherwise, a list of tables, each with the following fields
|
||||
-- cardData: TTS object data, suitable for spawning the card
|
||||
-- cardMetadata: Table of parsed metadata
|
||||
function getCardsByClassAndLevel(params)
|
||||
if (not indexingDone) then
|
||||
broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2})
|
||||
return { }
|
||||
end
|
||||
local upgradeKey
|
||||
if (params.upgraded) then
|
||||
upgradeKey = "-upgrade"
|
||||
else
|
||||
upgradeKey = "-level0"
|
||||
end
|
||||
return classAndLevelIndex[params.class..upgradeKey];
|
||||
end
|
||||
|
||||
-- Gets a random basic weakness from the bag. Once a given ID has been returned
|
||||
-- it will be removed from the list and cannot be selected again until a reload
|
||||
-- occurs or the indexes are rebuilt, which will refresh the list to include all
|
||||
-- weaknesses.
|
||||
-- Return: String ID of the selected weakness.
|
||||
function getRandomWeaknessId()
|
||||
local pickedIndex = math.random(#basicWeaknessList)
|
||||
local weaknessId = basicWeaknessList[pickedIndex]
|
||||
if (#basicWeaknessList > 1) then
|
||||
table.remove(basicWeaknessList, pickedIndex)
|
||||
else
|
||||
broadcastToAll("All weaknesses have been drawn!", {0.9, 0.2, 0.2})
|
||||
end
|
||||
|
||||
return weaknessId
|
||||
end
|
271
src/playercards/ClassCardContainer.ttslua
Normal file
271
src/playercards/ClassCardContainer.ttslua
Normal file
@ -0,0 +1,271 @@
|
||||
-- Class card bag implementation. Mimics the behavior of the previous SCED
|
||||
-- card memory bags, but spawns cards from the All Cards Bag instead.
|
||||
--
|
||||
-- The All Cards Bag handles indexing of the player cards by class and level, as
|
||||
-- well as sorting those lists. See that object for more information.
|
||||
|
||||
local allCardsBagGuid = "15bb07"
|
||||
|
||||
-- Lines which define the card layout area. The X threshold is shared, basic
|
||||
-- cards have Z > 47 while upgrades have Z < -46
|
||||
-- Note that the SCED table is rotated, making X the vertical (up the table)
|
||||
-- axis and Z the side-to-side axis
|
||||
local CARD_AREA_X_THRESHOLD = 22
|
||||
local CARD_AREA_BASIC_Z_THRESHOLD = 47
|
||||
local CARD_AREA_UPGRADED_Z_THRESHOLD = -46
|
||||
|
||||
local skillCount = 0
|
||||
local eventCount = 0
|
||||
local assetCount = 0
|
||||
|
||||
-- Coordinates to begin laying out cards to match the reserved areas of the
|
||||
-- table. Cards will lay out horizontally, then create additional rows
|
||||
local startPositions = {
|
||||
upgrades = {
|
||||
skill = Vector(58.09966, 1.36, -47.42),
|
||||
event = Vector(52.94421, 1.36, -47.42),
|
||||
asset = Vector(40.29005, 1.36, -47.42),
|
||||
},
|
||||
level0 = {
|
||||
skill = Vector(58.38383, 1.36, 92.39036),
|
||||
event = Vector(53.22857, 1.36, 92.44123),
|
||||
asset = Vector(40.9602, 1.36, 92.44869),
|
||||
},
|
||||
}
|
||||
|
||||
-- Amount to shift for the next card (zShift) or next row of cards (xShift)
|
||||
-- Note that the table rotation is weird, and the X axis is vertical while the
|
||||
-- Z axis is horizontal
|
||||
local zShift = -2.29998
|
||||
local xShift = -3.66572
|
||||
local yRotation = 270
|
||||
local cardsPerRow = 20
|
||||
|
||||
-- Tracks cards which are placed by this "bag" so they can be recalled
|
||||
local placedCardGuids = { }
|
||||
local placedCardsCount = 0
|
||||
|
||||
-- In order to mimic the behavior of the previous memory buttons we use a
|
||||
-- a temporary bag when recalling objects. This bag is tiny and transparent,
|
||||
-- and will be placed at the same location as this object. Once all placed
|
||||
-- cards are recalled bag to this bag, it will be destroyed
|
||||
local recallBag = {
|
||||
Name = "Bag",
|
||||
Transform = {
|
||||
scaleX = 0.01,
|
||||
scaleY = 0.01,
|
||||
scaleZ = 0.01,
|
||||
},
|
||||
ColorDiffuse = {
|
||||
r = 0,
|
||||
g = 0,
|
||||
b = 0,
|
||||
a = 0,
|
||||
},
|
||||
Locked = true,
|
||||
Grid = true,
|
||||
Snap = false,
|
||||
Tooltip = false,
|
||||
}
|
||||
|
||||
function onLoad(savedData)
|
||||
createPlaceRecallButtons()
|
||||
placedCardGuids = { }
|
||||
placedCardCount = 0
|
||||
if (savedData ~= nil) then
|
||||
local saveState = JSON.decode(savedData)
|
||||
if (saveState.placedCards ~= nil) then
|
||||
placedCardGuids = saveState.placedCards
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function onSave()
|
||||
local saveState = {
|
||||
placedCards = placedCardGuids,
|
||||
}
|
||||
|
||||
return JSON.encode(saveState)
|
||||
end
|
||||
|
||||
--Creates recall and place buttons
|
||||
function createPlaceRecallButtons()
|
||||
self.createButton({
|
||||
label="Place", click_function="buttonClick_place", function_owner=self,
|
||||
position={1,0.1,2.1}, rotation={0,0,0}, height=350, width=800,
|
||||
font_size=250, color={0,0,0}, font_color={1,1,1}
|
||||
})
|
||||
self.createButton({
|
||||
label="Recall", click_function="buttonClick_recall", function_owner=self,
|
||||
position={-1,0.1,2.1}, rotation={0,0,0}, height=350, width=800,
|
||||
font_size=250, color={0,0,0}, font_color={1,1,1}
|
||||
})
|
||||
end
|
||||
|
||||
-- Spawns the set of cards identified by this objects Name (which should hold
|
||||
-- the class) and description (whether to spawn basic cards or upgraded)
|
||||
function buttonClick_place()
|
||||
-- Cards already on the table, don't spawn more
|
||||
if (placedCardCount > 0) then
|
||||
return
|
||||
end
|
||||
local cardClass = self.getName()
|
||||
local isUpgraded = false
|
||||
if (self.getDescription() == "Upgrades") then
|
||||
isUpgraded = true
|
||||
end
|
||||
skillCount = 0
|
||||
eventCount = 0
|
||||
assetCount = 0
|
||||
local allCardsBag = getObjectFromGUID(allCardsBagGuid)
|
||||
local cardList = allCardsBag.call("getCardsByClassAndLevel", {class = cardClass, upgraded = isUpgraded})
|
||||
placeCards(cardList)
|
||||
end
|
||||
|
||||
-- Spawn all cards from the returned index
|
||||
function placeCards(cardIdList)
|
||||
local allCardsBag = getObjectFromGUID(allCardsBagGuid)
|
||||
local indexReady = allCardsBag.call("isIndexReady")
|
||||
if (not indexReady) then
|
||||
broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2})
|
||||
return
|
||||
end
|
||||
|
||||
for _, cardId in ipairs(cardIdList) do
|
||||
local card = allCardsBag.call("getCardById", { id = cardId })
|
||||
placeCard(card.data, card.metadata)
|
||||
end
|
||||
end
|
||||
|
||||
function placeCard(cardData, cardMetadata)
|
||||
local destinationPos
|
||||
if (cardMetadata.type == "Skill") then
|
||||
destinationPos = getSkillPosition(cardMetadata.level > 0)
|
||||
elseif (cardMetadata.type == "Event") then
|
||||
destinationPos = getEventPosition(cardMetadata.level > 0)
|
||||
elseif (cardMetadata.type == "Asset") then
|
||||
destinationPos = getAssetPosition(cardMetadata.level > 0)
|
||||
end
|
||||
-- Clear the GUID from the card's data so it will get a new GUID on spawn.
|
||||
-- This solves the issue of duplicate GUIDs being spawned and causing problems
|
||||
-- with recall
|
||||
cardData.GUID = nil
|
||||
local spawnedCard = spawnObjectData({
|
||||
data = cardData,
|
||||
position = destinationPos,
|
||||
rotation = {0, yRotation, 0},
|
||||
callback_function = recordPlacedCard})
|
||||
end
|
||||
|
||||
-- Returns the table position where the next skill should be placed
|
||||
-- Param isUpgraded: True if it's an upgraded card (right side of the table),
|
||||
-- false for a Level 0 card (left side of table)
|
||||
function getSkillPosition(isUpgraded)
|
||||
local skillPos
|
||||
if (isUpgraded) then
|
||||
skillPos = startPositions.upgrades.skill:copy()
|
||||
else
|
||||
skillPos = startPositions.level0.skill:copy()
|
||||
end
|
||||
local shift = Vector(div(skillCount, cardsPerRow) * xShift, 0, (skillCount % cardsPerRow) * zShift)
|
||||
skillPos:add(shift)
|
||||
skillCount = skillCount + 1
|
||||
|
||||
return skillPos
|
||||
end
|
||||
|
||||
-- Returns the table position where the next event should be placed
|
||||
-- Param isUpgraded: True if it's an upgraded card (right side of the table),
|
||||
-- false for a Level 0 card (left side of table)
|
||||
function getEventPosition(isUpgraded)
|
||||
local eventPos
|
||||
if (isUpgraded) then
|
||||
eventPos = startPositions.upgrades.event:copy()
|
||||
else
|
||||
eventPos = startPositions.level0.event:copy()
|
||||
end
|
||||
local shift = Vector(div(eventCount, cardsPerRow) * xShift, 0, (eventCount % cardsPerRow) * zShift)
|
||||
eventPos:add(shift)
|
||||
eventCount = eventCount + 1
|
||||
|
||||
return eventPos
|
||||
end
|
||||
|
||||
-- Returns the table position where the next asset should be placed
|
||||
-- Param isUpgraded: True if it's an upgraded card (right side of the table),
|
||||
-- false for a Level 0 card (left side of table)
|
||||
function getAssetPosition(isUpgraded)
|
||||
local assetPos
|
||||
if (isUpgraded) then
|
||||
assetPos = startPositions.upgrades.asset:copy()
|
||||
else
|
||||
assetPos = startPositions.level0.asset:copy()
|
||||
end
|
||||
local shift = Vector(div(assetCount, cardsPerRow) * xShift, 0, (assetCount % cardsPerRow) * zShift)
|
||||
assetPos:add(shift)
|
||||
assetCount = assetCount + 1
|
||||
|
||||
return assetPos
|
||||
end
|
||||
|
||||
-- Callback function which adds a spawned card to the tracking list
|
||||
function recordPlacedCard(spawnedCard)
|
||||
if (spawnedCard.getName() == "Protecting the Anirniq (2)") then
|
||||
log("Spawned PtA "..spawnedCard.getGUID())
|
||||
end
|
||||
placedCardGuids[spawnedCard.getGUID()] = true
|
||||
placedCardCount = placedCardCount + 1
|
||||
end
|
||||
|
||||
-- Recalls all spawned cards to the bag, and clears the placedCardGuids list
|
||||
function buttonClick_recall()
|
||||
local trash = spawnObjectData({data = recallBag, position = self.getPosition()})
|
||||
for cardGuid, _ in pairs(placedCardGuids) do
|
||||
local card = getObjectFromGUID(cardGuid)
|
||||
if (card ~= nil) then
|
||||
trash.putObject(card)
|
||||
placedCardGuids[cardGuid] = nil
|
||||
placedCardCount = placedCardCount - 1
|
||||
end
|
||||
end
|
||||
|
||||
if (placedCardCount > 0) then
|
||||
-- Couldn't recall all the cards, check and pull them from decks
|
||||
local decksInArea = { }
|
||||
local allObjects = getAllObjects()
|
||||
for _, object in ipairs(allObjects) do
|
||||
if (object.name == "Deck" and isInArea(object)) then
|
||||
table.insert(decksInArea, object)
|
||||
end
|
||||
end
|
||||
for _, deck in ipairs(decksInArea) do
|
||||
local cardsInDeck = deck.getObjects()
|
||||
for i, card in ipairs(cardsInDeck) do
|
||||
if (placedCardGuids[card.guid]) then
|
||||
trash.putObject(deck.takeObject({ guid = card.guid }))
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
trash.destruct()
|
||||
-- We've recalled everything we can, some cards may have been moved out of the
|
||||
-- card area. Just reset at this point.
|
||||
placedCardGuids = { }
|
||||
placedCardCount = 0
|
||||
end
|
||||
|
||||
function isInArea(object)
|
||||
if (object == nil) then
|
||||
return false
|
||||
end
|
||||
local position = object.getPosition()
|
||||
return position.x > CARD_AREA_X_THRESHOLD
|
||||
and (position.z > CARD_AREA_BASIC_Z_THRESHOLD
|
||||
or position.z < CARD_AREA_UPGRADED_Z_THRESHOLD)
|
||||
end
|
||||
|
||||
function div(a,b)
|
||||
return (a - a % b) / b
|
||||
end
|
24
src/playercards/RandomWeaknessGenerator.ttslua
Normal file
24
src/playercards/RandomWeaknessGenerator.ttslua
Normal file
@ -0,0 +1,24 @@
|
||||
local allCardsBagGuid = "15bb07"
|
||||
|
||||
function onLoad(saved_data)
|
||||
createDrawButton()
|
||||
end
|
||||
|
||||
function createDrawButton()
|
||||
self.createButton({
|
||||
label="Draw Random\nWeakness", click_function="buttonClick_draw", function_owner=self,
|
||||
position={0,0.1,2.1}, rotation={0,0,0}, height=600, width=1800,
|
||||
font_size=250, color={0,0,0}, font_color={1,1,1}
|
||||
})
|
||||
end
|
||||
|
||||
-- Draw a random weakness and spawn it below the object
|
||||
function buttonClick_draw()
|
||||
local allCardsBag = getObjectFromGUID(allCardsBagGuid)
|
||||
local weaknessId = allCardsBag.call("getRandomWeaknessId")
|
||||
local card = allCardsBag.call("getCardById", { id = weaknessId })
|
||||
spawnObjectData({
|
||||
data = card.data,
|
||||
position = self.positionToWorld({0, 1, 5.5}),
|
||||
rotation = self.getRotation()})
|
||||
end
|
90
src/playercards/ScriptedTarot.ttslua
Normal file
90
src/playercards/ScriptedTarot.ttslua
Normal file
@ -0,0 +1,90 @@
|
||||
CARD_OFFSET = Vector({0, 0.1, -2})
|
||||
ORIENTATIONS = { {0, 270, 0}, { 0, 90, 0} }
|
||||
READING = {
|
||||
"Temperance",
|
||||
"Justice",
|
||||
"Hermit",
|
||||
"Hanged Man",
|
||||
"Hierophant",
|
||||
"Lovers",
|
||||
"Chariot",
|
||||
"Wheel of Fortune"
|
||||
}
|
||||
|
||||
function onLoad()
|
||||
self.addContextMenuItem("Chaos", chaos, false)
|
||||
self.addContextMenuItem("Balance", balance, false)
|
||||
self.addContextMenuItem("Choice", choice, false)
|
||||
self.addContextMenuItem("Destiny (Campaign)", destiny, false)
|
||||
self.addContextMenuItem("Accept Your Fate", fate, false)
|
||||
|
||||
math.randomseed(os.time())
|
||||
end
|
||||
|
||||
function chaos(color)
|
||||
self.shuffle()
|
||||
self.takeObject({
|
||||
position = self.getPosition() + CARD_OFFSET,
|
||||
rotation = ORIENTATIONS[math.random(2)],
|
||||
smooth = true
|
||||
})
|
||||
end
|
||||
|
||||
function balance(color)
|
||||
self.shuffle()
|
||||
self.takeObject({
|
||||
position = self.getPosition() + CARD_OFFSET,
|
||||
rotation = ORIENTATIONS[1],
|
||||
smooth = true
|
||||
})
|
||||
self.takeObject({
|
||||
position = self.getPosition() + 2*CARD_OFFSET,
|
||||
rotation = ORIENTATIONS[2],
|
||||
smooth = true
|
||||
})
|
||||
end
|
||||
|
||||
function choice(color)
|
||||
self.shuffle()
|
||||
for i=1,3 do
|
||||
self.takeObject({
|
||||
position = self.getPosition() + i*CARD_OFFSET,
|
||||
rotation = ORIENTATIONS[1],
|
||||
smooth = true
|
||||
})
|
||||
end
|
||||
broadcastToColor("Choose and reverse two of the cards.", color)
|
||||
end
|
||||
|
||||
function destiny(color)
|
||||
self.shuffle()
|
||||
for i=1,8 do
|
||||
self.takeObject({
|
||||
position = self.getPosition() + i*CARD_OFFSET,
|
||||
rotation = ORIENTATIONS[1],
|
||||
smooth = true
|
||||
})
|
||||
end
|
||||
broadcastToColor("Each card corresponds to one scenario, leftmost is first. Choose and reverse half of the cards (rounded up).", color)
|
||||
end
|
||||
|
||||
function fate(color)
|
||||
local guids = {}
|
||||
local cards = self.getObjects()
|
||||
for i,card in ipairs(cards) do
|
||||
for j,reading in ipairs(READING) do
|
||||
if string.match(card.name, reading) ~= nil then
|
||||
guids[j] = card.guid
|
||||
end
|
||||
end
|
||||
end
|
||||
for k,guid in ipairs(guids) do
|
||||
self.takeObject({
|
||||
guid = guid,
|
||||
position = self.getPosition() + k*CARD_OFFSET,
|
||||
rotation = ORIENTATIONS[1],
|
||||
smooth = true
|
||||
})
|
||||
end
|
||||
broadcastToColor("Each card corresponds to one scenario, leftmost is first. Choose and reverse half of the cards (rounded up).", color)
|
||||
end
|
114
src/playermat/ClueCounter.ttslua
Normal file
114
src/playermat/ClueCounter.ttslua
Normal file
@ -0,0 +1,114 @@
|
||||
--Counting Bowl by MrStump
|
||||
|
||||
--Table of items which can be counted in this Bowl
|
||||
--Each entry has 2 things to enter
|
||||
--a name (what is in the name field of that object)
|
||||
--a value (how much it is worth)
|
||||
--A number in the items description will override the number entry in this table
|
||||
validCountItemList = {
|
||||
["Clue"] = 1,
|
||||
[""] = 1,
|
||||
--["Name3"] = 2,
|
||||
--["Name4"] = 31,
|
||||
--Add more entries as needed
|
||||
--Remove the -- from before a line for the script to use it
|
||||
}
|
||||
|
||||
--END OF CODE TO EDIT
|
||||
|
||||
function onLoad()
|
||||
timerID = self.getGUID()..math.random(9999999999999)
|
||||
--Sets position/color for the button, spawns it
|
||||
self.createButton({
|
||||
label="", click_function="removeAllClues", function_owner=self,
|
||||
position={0,0,0}, rotation={0,8,0}, height=0, width=0,
|
||||
font_color={0,0,0}, font_size=2000
|
||||
})
|
||||
--Start timer which repeats forever, running countItems() every second
|
||||
Timer.create({
|
||||
identifier=timerID,
|
||||
function_name="countItems", function_owner=self,
|
||||
repetitions=0, delay=1
|
||||
})
|
||||
exposedValue = 0
|
||||
trashCan = getObjectFromGUID("147e80")
|
||||
end
|
||||
|
||||
function findValidItemsInSphere()
|
||||
return filterByValidity(findItemsInSphere())
|
||||
end
|
||||
|
||||
--Activated once per second, counts items in bowls
|
||||
function countItems()
|
||||
local totalValue = -1
|
||||
local countableItems = findValidItemsInSphere()
|
||||
for ind, entry in ipairs(countableItems) do
|
||||
local descValue = tonumber(entry.hit_object.getDescription())
|
||||
local stackMult = math.abs(entry.hit_object.getQuantity())
|
||||
--Use value in description if available
|
||||
if descValue ~= nil then
|
||||
totalValue = totalValue + descValue * stackMult
|
||||
else
|
||||
--Otherwise use the value in validCountItemList
|
||||
totalValue = totalValue + validCountItemList[entry.hit_object.getName()] * stackMult
|
||||
end
|
||||
end
|
||||
exposedValue = totalValue
|
||||
--Updates the number display
|
||||
self.editButton({index=0, label=totalValue})
|
||||
end
|
||||
|
||||
function filterByValidity(items)
|
||||
retval = {}
|
||||
for _, entry in ipairs(items) do
|
||||
--Ignore the bowl
|
||||
if entry.hit_object ~= self then
|
||||
--Ignore if not in validCountItemList
|
||||
local tableEntry = validCountItemList[entry.hit_object.getName()]
|
||||
if tableEntry ~= nil then
|
||||
table.insert(retval, entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
return retval
|
||||
end
|
||||
|
||||
|
||||
--Gets the items in the bowl for countItems to count
|
||||
function findItemsInSphere()
|
||||
--Find scaling factor
|
||||
local scale = self.getScale()
|
||||
--Set position for the sphere
|
||||
local pos = self.getPosition()
|
||||
pos.y=pos.y+(1.25*scale.y)
|
||||
--Ray trace to get all objects
|
||||
return Physics.cast({
|
||||
origin=pos, direction={0,1,0}, type=2, max_distance=0,
|
||||
size={6*scale.x,6*scale.y,6*scale.z}, --debug=true
|
||||
})
|
||||
end
|
||||
|
||||
function removeAllClues()
|
||||
startLuaCoroutine(self, "clueRemovalCoroutine")
|
||||
end
|
||||
|
||||
function clueRemovalCoroutine()
|
||||
for _, entry in ipairs(findValidItemsInSphere()) do
|
||||
-- Do not put the table in the garbage
|
||||
if entry.hit_object.getGUID() ~= "4ee1f2" then
|
||||
--delay for animation purposes
|
||||
for k=1,10 do
|
||||
coroutine.yield(0)
|
||||
end
|
||||
trashCan.putObject(entry.hit_object)
|
||||
end
|
||||
end
|
||||
--coroutines must return a value
|
||||
return 1
|
||||
end
|
||||
|
||||
function onDestroy()
|
||||
if timerID and type(timerID) == 'object' then
|
||||
Timer.destroy(timerID)
|
||||
end
|
||||
end
|
132
src/playermat/DamageTracker.ttslua
Normal file
132
src/playermat/DamageTracker.ttslua
Normal file
@ -0,0 +1,132 @@
|
||||
MIN_VALUE = -99
|
||||
MAX_VALUE = 999
|
||||
|
||||
function onload(saved_data)
|
||||
light_mode = true
|
||||
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,0,0,100}
|
||||
|
||||
if light_mode then
|
||||
f_color = {1,1,1,100}
|
||||
else
|
||||
f_color = {0,0,0,100}
|
||||
end
|
||||
|
||||
|
||||
|
||||
self.createButton({
|
||||
label=tostring(val),
|
||||
click_function="add_subtract",
|
||||
function_owner=self,
|
||||
position={0.1,0.05,0.1},
|
||||
height=600,
|
||||
width=1000,
|
||||
alignment = 3,
|
||||
scale={x=1.5, y=1.5, z=1.5},
|
||||
font_size=600,
|
||||
font_color=f_color,
|
||||
color={1,1,1,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
|
132
src/playermat/HorrorTracker.ttslua
Normal file
132
src/playermat/HorrorTracker.ttslua
Normal file
@ -0,0 +1,132 @@
|
||||
MIN_VALUE = -99
|
||||
MAX_VALUE = 999
|
||||
|
||||
function onload(saved_data)
|
||||
light_mode = true
|
||||
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,0,0,100}
|
||||
|
||||
if light_mode then
|
||||
f_color = {1,1,1,100}
|
||||
else
|
||||
f_color = {0,0,0,100}
|
||||
end
|
||||
|
||||
|
||||
|
||||
self.createButton({
|
||||
label=tostring(val),
|
||||
click_function="add_subtract",
|
||||
function_owner=self,
|
||||
position={-0.025,0.05,-0.025},
|
||||
height=600,
|
||||
width=1000,
|
||||
alignment = 3,
|
||||
scale={x=1.5, y=1.5, z=1.5},
|
||||
font_size=600,
|
||||
font_color=f_color,
|
||||
color={1,1,1,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
|
480
src/playermat/PlaymatGreen.ttslua
Normal file
480
src/playermat/PlaymatGreen.ttslua
Normal file
@ -0,0 +1,480 @@
|
||||
-- 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
|
480
src/playermat/PlaymatOrange.ttslua
Normal file
480
src/playermat/PlaymatOrange.ttslua
Normal file
@ -0,0 +1,480 @@
|
||||
-- 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
|
480
src/playermat/PlaymatRed.ttslua
Normal file
480
src/playermat/PlaymatRed.ttslua
Normal file
@ -0,0 +1,480 @@
|
||||
-- set true to enable debug logging
|
||||
DEBUG = false
|
||||
-- we use this to turn off collision handling (for clue spawning)
|
||||
-- until after load is complete (probably a better way to do this)
|
||||
COLLISION_ENABLED = false
|
||||
-- position offsets, adjust these to reposition things relative to mat [x,y,z]
|
||||
DRAWN_ENCOUNTER_CARD_OFFSET = {0.98, 0.5, -0.635}
|
||||
DRAWN_CHAOS_TOKEN_OFFSET = {-1.2, 0.5, -0.45}
|
||||
DISCARD_BUTTON_OFFSETS = {
|
||||
{-0.98, 0.2, -0.945},
|
||||
{-0.525, 0.2, -0.945},
|
||||
{-0.07, 0.2, -0.945},
|
||||
{0.39, 0.2, -0.945},
|
||||
{0.84, 0.2, -0.945},
|
||||
}
|
||||
-- draw deck and discard zone
|
||||
DECK_POSITION = { x=-1.4, y=0, z=0.3 }
|
||||
DECK_ZONE_SCALE = { x=3, y=5, z=8 }
|
||||
DRAW_DECK_POSITION = { x=-18.9, y=2.5, z=-26.7 }
|
||||
|
||||
-- play zone
|
||||
PLAYER_COLOR = "Red"
|
||||
PLAY_ZONE_POSITION = { x=-25, y=4, z=-27 }
|
||||
PLAY_ZONE_ROTATION = { x=0, y=180, z=0 }
|
||||
PLAY_ZONE_SCALE = { x=30, y=5, z=15 }
|
||||
|
||||
RESOURCE_COUNTER_GUID = "a4b60d"
|
||||
|
||||
-- the position of the global discard pile
|
||||
-- TODO: delegate to global for any auto discard actions
|
||||
DISCARD_POSITION = {-3.85, 3, 10.38}
|
||||
|
||||
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
|
480
src/playermat/PlaymatWhite.ttslua
Normal file
480
src/playermat/PlaymatWhite.ttslua
Normal file
@ -0,0 +1,480 @@
|
||||
-- set true to enable debug logging
|
||||
DEBUG = false
|
||||
-- we use this to turn off collision handling (for clue spawning)
|
||||
-- until after load is complete (probably a better way to do this)
|
||||
COLLISION_ENABLED = false
|
||||
-- position offsets, adjust these to reposition things relative to mat [x,y,z]
|
||||
DRAWN_ENCOUNTER_CARD_OFFSET = {0.98, 0.5, -0.635}
|
||||
DRAWN_CHAOS_TOKEN_OFFSET = {-1.2, 0.5, -0.45}
|
||||
DISCARD_BUTTON_OFFSETS = {
|
||||
{-0.98, 0.2, -0.945},
|
||||
{-0.525, 0.2, -0.945},
|
||||
{-0.07, 0.2, -0.945},
|
||||
{0.39, 0.2, -0.945},
|
||||
{0.84, 0.2, -0.945},
|
||||
}
|
||||
-- draw deck and discard zone
|
||||
DECK_POSITION = { x=-1.4, y=0, z=0.3 }
|
||||
DECK_ZONE_SCALE = { x=3, y=5, z=8 }
|
||||
DRAW_DECK_POSITION = { x=-55, y=2.5, z=4.5 }
|
||||
|
||||
-- play zone
|
||||
PLAYER_COLOR = "White"
|
||||
PLAY_ZONE_POSITION = { x=-54.42, y=4.10, z=20.96}
|
||||
PLAY_ZONE_ROTATION = { x=0, y=270, z=0 }
|
||||
PLAY_ZONE_SCALE = { x=36.63, y=5.10, z=14.59}
|
||||
|
||||
RESOURCE_COUNTER_GUID = "4406f0"
|
||||
|
||||
-- the position of the global discard pile
|
||||
-- TODO: delegate to global for any auto discard actions
|
||||
DISCARD_POSITION = {-3.85, 3, 10.38}
|
||||
|
||||
function log(message)
|
||||
if DEBUG then
|
||||
print(message)
|
||||
end
|
||||
end
|
||||
|
||||
-- builds a function that discards things in searchPostion to discardPostition
|
||||
function makeDiscardHandlerFor(searchPosition, discardPosition)
|
||||
return function (_)
|
||||
local discardItemList = findObjectsAtPosition(searchPosition)
|
||||
for _, obj in ipairs(discardItemList) do
|
||||
obj.setPositionSmooth(discardPosition, false, true)
|
||||
obj.setRotation({0, -90, 0})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- build a discard button at position to discard from searchPosition to discardPosition
|
||||
-- number must be unique
|
||||
function makeDiscardButton(position, searchPosition, discardPosition, number)
|
||||
local handler = makeDiscardHandlerFor(searchPosition, discardPosition)
|
||||
local handlerName = 'handler' .. number
|
||||
self.setVar(handlerName, handler)
|
||||
self.createButton({
|
||||
label = "Discard",
|
||||
click_function= handlerName,
|
||||
function_owner= self,
|
||||
position = position,
|
||||
scale = {0.12, 0.12, 0.12},
|
||||
width = 800,
|
||||
height = 280,
|
||||
font_size = 180,
|
||||
})
|
||||
end
|
||||
|
||||
function onload(save_state)
|
||||
self.interactable = DEBUG
|
||||
DATA_HELPER = getObjectFromGUID('708279')
|
||||
PLAYER_CARDS = DATA_HELPER.getTable('PLAYER_CARD_DATA')
|
||||
PLAYER_CARD_TOKEN_OFFSETS = DATA_HELPER.getTable('PLAYER_CARD_TOKEN_OFFSETS')
|
||||
|
||||
-- positions of encounter card slots
|
||||
local encounterSlots = {
|
||||
{1, 0, -0.7},
|
||||
{0.55, 0, -0.7},
|
||||
{0.1, 0, -0.7},
|
||||
{-0.35, 0, -0.7},
|
||||
{-0.8, 0, -0.7}
|
||||
}
|
||||
|
||||
local i = 1
|
||||
while i <= 5 do
|
||||
makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], encounterSlots[i], DISCARD_POSITION, i)
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
self.createButton({
|
||||
label = " ",
|
||||
click_function = "drawEncountercard",
|
||||
function_owner = self,
|
||||
position = {-1.45,0,-0.7},
|
||||
rotation = {0,-15,0},
|
||||
width = 170,
|
||||
height = 255,
|
||||
font_size = 50
|
||||
})
|
||||
|
||||
self.createButton({
|
||||
label=" ",
|
||||
click_function = "drawChaostokenButton",
|
||||
function_owner = self,
|
||||
position = {1.48,0.0,-0.74},
|
||||
rotation = {0,-45,0},
|
||||
width = 125,
|
||||
height = 125,
|
||||
font_size = 50
|
||||
})
|
||||
|
||||
self.createButton({
|
||||
label="Upkeep",
|
||||
click_function = "doUpkeep",
|
||||
function_owner = self,
|
||||
position = {1.48,0.1,-0.44},
|
||||
scale = {0.12, 0.12, 0.12},
|
||||
width = 800,
|
||||
height = 280,
|
||||
font_size = 180
|
||||
})
|
||||
|
||||
-- self.createButton({
|
||||
-- label="Draw 1",
|
||||
-- click_function = "doDrawOne",
|
||||
-- function_owner = self,
|
||||
-- position = {1.48,0.1,-0.36},
|
||||
-- scale = {0.12, 0.12, 0.12},
|
||||
-- width = 800,
|
||||
-- height = 280,
|
||||
-- font_size = 180
|
||||
-- })
|
||||
|
||||
local state = JSON.decode(save_state)
|
||||
if state ~= nil then
|
||||
if state.playerColor ~= nil then
|
||||
PLAYER_COLOR = state.playerColor
|
||||
end
|
||||
if state.zoneID ~= nil then
|
||||
zoneID = state.zoneID
|
||||
Wait.time(checkDeckZoneExists, 30)
|
||||
else
|
||||
spawnDeckZone()
|
||||
end
|
||||
else
|
||||
spawnDeckZone()
|
||||
end
|
||||
|
||||
COLLISION_ENABLED = true
|
||||
end
|
||||
|
||||
function onSave()
|
||||
return JSON.encode({ zoneID=zoneID, playerColor=PLAYER_COLOR })
|
||||
end
|
||||
|
||||
function setMessageColor(color)
|
||||
-- send messages to player who clicked button if no seated player found
|
||||
messageColor = Player[PLAYER_COLOR].seated and PLAYER_COLOR or color
|
||||
end
|
||||
|
||||
function getDrawDiscardDecks(zone)
|
||||
-- get the draw deck and discard pile objects
|
||||
drawDeck = nil
|
||||
discardPile = nil
|
||||
for i,object in ipairs(zone.getObjects()) do
|
||||
if object.tag == "Deck" or object.tag == "Card" then
|
||||
if object.is_face_down then
|
||||
drawDeck = object
|
||||
else
|
||||
discardPile = object
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function checkDeckThenDrawOne()
|
||||
-- draw 1 card, shuffling the discard pile if necessary
|
||||
if drawDeck == nil then
|
||||
if discardPile ~= nil then
|
||||
shuffleDiscardIntoDeck()
|
||||
Wait.time(|| drawCards(1), 1)
|
||||
end
|
||||
printToColor("Take 1 horror (drawing card from empty deck)", messageColor)
|
||||
else
|
||||
drawCards(1)
|
||||
end
|
||||
end
|
||||
|
||||
function doUpkeep(obj, color, alt_click)
|
||||
-- right-click binds to new player color
|
||||
if alt_click then
|
||||
PLAYER_COLOR = color
|
||||
printToColor("Upkeep button bound to " .. color, color)
|
||||
return
|
||||
end
|
||||
|
||||
setMessageColor(color)
|
||||
|
||||
-- unexhaust cards in play zone
|
||||
local objs = Physics.cast({
|
||||
origin = PLAY_ZONE_POSITION,
|
||||
direction = { x=0, y=1, z=0 },
|
||||
type = 3,
|
||||
size = PLAY_ZONE_SCALE,
|
||||
orientation = PLAY_ZONE_ROTATION
|
||||
})
|
||||
|
||||
local y = PLAY_ZONE_ROTATION.y
|
||||
|
||||
local investigator = nil
|
||||
for i,v in ipairs(objs) do
|
||||
local obj = v.hit_object
|
||||
local props = obj.getCustomObject()
|
||||
if obj.tag == "Card" and not obj.is_face_down and not doNotReady(obj) then
|
||||
if props ~= nil and props.unique_back then
|
||||
local name = obj.getName()
|
||||
if string.match(name, "Jenny Barnes") ~= nil then
|
||||
investigator = "Jenny Barnes"
|
||||
elseif name == "Patrice Hathaway" then
|
||||
investigator = name
|
||||
end
|
||||
else
|
||||
local r = obj.getRotation()
|
||||
if (r.y - y > 10) or (y - r.y > 10) then
|
||||
obj.setRotation(PLAY_ZONE_ROTATION)
|
||||
end
|
||||
end
|
||||
elseif obj.tag == "Board" and obj.getDescription() == "Action token" then
|
||||
if obj.is_face_down then obj.flip() end
|
||||
end
|
||||
end
|
||||
|
||||
-- gain resource
|
||||
getObjectFromGUID(RESOURCE_COUNTER_GUID).call("add_subtract")
|
||||
if investigator == "Jenny Barnes" then
|
||||
getObjectFromGUID(RESOURCE_COUNTER_GUID).call("add_subtract")
|
||||
printToColor("Taking 2 resources (Jenny)", messageColor)
|
||||
end
|
||||
|
||||
-- get the draw deck and discard pile objects
|
||||
local zone = getObjectFromGUID(zoneID)
|
||||
if zone == nil then return end
|
||||
|
||||
getDrawDiscardDecks(zone)
|
||||
|
||||
-- special draw for Patrice Hathaway (shuffle discards if necessary)
|
||||
if investigator == "Patrice Hathaway" then
|
||||
patriceDraw()
|
||||
return
|
||||
end
|
||||
|
||||
-- draw 1 card (shuffle discards if necessary)
|
||||
checkDeckThenDrawOne()
|
||||
end
|
||||
|
||||
function doDrawOne(obj, color, alt_click)
|
||||
-- right-click binds to new player color
|
||||
if alt_click then
|
||||
PLAYER_COLOR = color
|
||||
printToColor("Draw 1 button bound to " .. color, color)
|
||||
return
|
||||
end
|
||||
|
||||
setMessageColor(color)
|
||||
|
||||
-- get the draw deck and discard pile objects
|
||||
local zone = getObjectFromGUID(zoneID)
|
||||
if zone == nil then return end
|
||||
|
||||
getDrawDiscardDecks(zone)
|
||||
|
||||
-- draw 1 card (shuffle discards if necessary)
|
||||
checkDeckThenDrawOne()
|
||||
end
|
||||
|
||||
function doNotReady(card)
|
||||
if card.getVar("do_not_ready") == true then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function drawCards(numCards)
|
||||
if drawDeck == nil then return end
|
||||
drawDeck.deal(numCards, PLAYER_COLOR)
|
||||
end
|
||||
|
||||
function shuffleDiscardIntoDeck()
|
||||
discardPile.flip()
|
||||
discardPile.shuffle()
|
||||
discardPile.setPositionSmooth(DRAW_DECK_POSITION, false, false)
|
||||
drawDeck = discardPile
|
||||
discardPile = nil
|
||||
end
|
||||
|
||||
function patriceDraw()
|
||||
local handSize = #Player[PLAYER_COLOR].getHandObjects()
|
||||
if handSize >= 5 then return end
|
||||
local cardsToDraw = 5 - handSize
|
||||
local deckSize
|
||||
printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor)
|
||||
if drawDeck == nil then
|
||||
deckSize = 0
|
||||
elseif drawDeck.tag == "Deck" then
|
||||
deckSize = #drawDeck.getObjects()
|
||||
else
|
||||
deckSize = 1
|
||||
end
|
||||
|
||||
if deckSize >= cardsToDraw then
|
||||
drawCards(cardsToDraw)
|
||||
return
|
||||
end
|
||||
|
||||
drawCards(deckSize)
|
||||
if discardPile ~= nil then
|
||||
shuffleDiscardIntoDeck()
|
||||
Wait.time(|| drawCards(cardsToDraw - deckSize), 1)
|
||||
end
|
||||
printToColor("Take 1 horror (drawing card from empty deck)", messageColor)
|
||||
end
|
||||
|
||||
function checkDeckZoneExists()
|
||||
if getObjectFromGUID(zoneID) ~= nil then return end
|
||||
spawnDeckZone()
|
||||
end
|
||||
|
||||
function spawnDeckZone()
|
||||
local pos = self.positionToWorld(DECK_POSITION)
|
||||
local zoneProps = {
|
||||
position = pos,
|
||||
scale = DECK_ZONE_SCALE,
|
||||
type = 'ScriptingTrigger',
|
||||
callback = 'zoneCallback',
|
||||
callback_owner = self,
|
||||
rotation = self.getRotation()
|
||||
}
|
||||
spawnObject(zoneProps)
|
||||
end
|
||||
|
||||
function zoneCallback(zone)
|
||||
zoneID = zone.getGUID()
|
||||
end
|
||||
|
||||
function findObjectsAtPosition(localPos)
|
||||
local globalPos = self.positionToWorld(localPos)
|
||||
local objList = Physics.cast({
|
||||
origin=globalPos, --Where the cast takes place
|
||||
direction={0,1,0}, --Which direction it moves (up is shown)
|
||||
type=2, --Type. 2 is "sphere"
|
||||
size={2,2,2}, --How large that sphere is
|
||||
max_distance=1, --How far it moves. Just a little bit
|
||||
debug=false --If it displays the sphere when casting.
|
||||
})
|
||||
local decksAndCards = {}
|
||||
for _, obj in ipairs(objList) do
|
||||
if obj.hit_object.tag == "Deck" or obj.hit_object.tag == "Card" then
|
||||
table.insert(decksAndCards, obj.hit_object)
|
||||
end
|
||||
end
|
||||
return decksAndCards
|
||||
end
|
||||
|
||||
function spawnTokenOn(object, offsets, tokenType)
|
||||
local tokenPosition = object.positionToWorld(offsets)
|
||||
spawnToken(tokenPosition, tokenType)
|
||||
end
|
||||
|
||||
-- spawn a group of tokens of the given type on the object
|
||||
function spawnTokenGroup(object, tokenType, tokenCount)
|
||||
local offsets = PLAYER_CARD_TOKEN_OFFSETS[tokenCount]
|
||||
if offsets == nil then
|
||||
error("couldn't find offsets for " .. tokenCount .. ' tokens')
|
||||
end
|
||||
local i = 0
|
||||
while i < tokenCount do
|
||||
local offset = offsets[i + 1]
|
||||
spawnTokenOn(object, offset, tokenType)
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
function buildPlayerCardKey(object)
|
||||
return object.getName() .. ':' .. object.getDescription()
|
||||
end
|
||||
|
||||
function getPlayerCardData(object)
|
||||
return PLAYER_CARDS[buildPlayerCardKey(object)] or PLAYER_CARDS[object.getName()]
|
||||
end
|
||||
|
||||
function shouldSpawnTokens(object)
|
||||
-- we assume we shouldn't spawn tokens if in doubt, this should
|
||||
-- only ever happen on load and in that case prevents respawns
|
||||
local spawned = DATA_HELPER.call('getSpawnedPlayerCardGuid', {object.getGUID()})
|
||||
local canSpawn = getPlayerCardData(object)
|
||||
return not spawned and canSpawn
|
||||
end
|
||||
|
||||
function markSpawned(object)
|
||||
local saved = DATA_HELPER.call('setSpawnedPlayerCardGuid', {object.getGUID(), true})
|
||||
if not saved then
|
||||
error('attempt to mark player card spawned before data loaded')
|
||||
end
|
||||
end
|
||||
|
||||
function spawnTokensFor(object)
|
||||
local data = getPlayerCardData(object)
|
||||
if data == nil then
|
||||
error('attempt to spawn tokens for ' .. object.getName() .. ': no token data')
|
||||
end
|
||||
log(object.getName() .. '[' .. object.getDescription() .. ']' .. ' : ' .. data['tokenType'] .. ' : ' .. data['tokenCount'])
|
||||
spawnTokenGroup(object, data['tokenType'], data['tokenCount'])
|
||||
markSpawned(object)
|
||||
end
|
||||
|
||||
function resetSpawnState()
|
||||
local zone = getObjectFromGUID(zoneID)
|
||||
if zone == nil then return end
|
||||
|
||||
for i,object in ipairs(zone.getObjects()) do
|
||||
if object.tag == "Card" then
|
||||
local guid = object.getGUID()
|
||||
if guid ~= nil then unmarkSpawned(guid, true) end
|
||||
elseif object.tag == "Deck" then
|
||||
local cards = object.getObjects()
|
||||
if (cards ~= nil) then
|
||||
for i,v in ipairs(cards) do
|
||||
if v.guid ~= nil then unmarkSpawned(v.guid) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function unmarkSpawned(guid, force)
|
||||
if not force and getObjectFromGUID(guid) ~= nil then return end
|
||||
DATA_HELPER.call('setSpawnedPlayerCardGuid', {guid, false})
|
||||
end
|
||||
|
||||
function onCollisionEnter(collision_info)
|
||||
if not COLLISION_ENABLED then
|
||||
return
|
||||
end
|
||||
|
||||
local object = collision_info.collision_object
|
||||
Wait.time(resetSpawnState, 1)
|
||||
-- anything to the left of this is legal to spawn
|
||||
local discardSpawnBoundary = self.positionToWorld({-1.2, 0, 0})
|
||||
local boundaryLocalToCard = object.positionToLocal(discardSpawnBoundary)
|
||||
if boundaryLocalToCard.x > 0 then
|
||||
log('not checking for token spawn, boundary relative is ' .. boundaryLocalToCard.x)
|
||||
return
|
||||
end
|
||||
if not object.is_face_down and shouldSpawnTokens(object) then
|
||||
spawnTokensFor(object)
|
||||
end
|
||||
end
|
||||
|
||||
-- functions delegated to Global
|
||||
function drawChaostokenButton(object, player, isRightClick)
|
||||
-- local toPosition = self.positionToWorld(DRAWN_CHAOS_TOKEN_OFFSET)
|
||||
Global.call("drawChaostoken", {self, DRAWN_CHAOS_TOKEN_OFFSET, isRightClick})
|
||||
end
|
||||
|
||||
function drawEncountercard(object, player, isRightClick)
|
||||
local toPosition = self.positionToWorld(DRAWN_ENCOUNTER_CARD_OFFSET)
|
||||
Global.call("drawEncountercard", {toPosition, self.getRotation(), isRightClick})
|
||||
end
|
||||
|
||||
function spawnToken(position, tokenType)
|
||||
Global.call('spawnToken', {position, tokenType})
|
||||
end
|
||||
|
||||
function updatePlayerCards(args)
|
||||
local custom_data_helper = getObjectFromGUID(args[1])
|
||||
data_player_cards = custom_data_helper.getTable("PLAYER_CARD_DATA")
|
||||
for k, v in pairs(data_player_cards) do
|
||||
PLAYER_CARDS[k] = v
|
||||
end
|
||||
end
|
132
src/playermat/ResourceTracker.ttslua
Normal file
132
src/playermat/ResourceTracker.ttslua
Normal file
@ -0,0 +1,132 @@
|
||||
MIN_VALUE = -99
|
||||
MAX_VALUE = 999
|
||||
|
||||
function onload(saved_data)
|
||||
light_mode = true
|
||||
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,0,0,100}
|
||||
|
||||
if light_mode then
|
||||
f_color = {1,1,1,100}
|
||||
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.1},
|
||||
height=600,
|
||||
width=1000,
|
||||
alignment = 3,
|
||||
scale={x=1.5, y=1.5, z=1.5},
|
||||
font_size=600,
|
||||
font_color=f_color,
|
||||
color={1,1,1,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
|
336
src/tokens/BlessCurseManager.ttslua
Normal file
336
src/tokens/BlessCurseManager.ttslua
Normal file
@ -0,0 +1,336 @@
|
||||
BLESS_COLOR = { r=0.3, g=0.25, b=0.09 }
|
||||
CURSE_COLOR = { r=0.2, g=0.08, b=0.24 }
|
||||
MIN_VALUE = 1
|
||||
MAX_VALUE = 10
|
||||
IMAGE_URL = {
|
||||
Bless = "http://cloud-3.steamusercontent.com/ugc/1655601092778627699/339FB716CB25CA6025C338F13AFDFD9AC6FA8356/",
|
||||
Curse = "http://cloud-3.steamusercontent.com/ugc/1655601092778636039/2A25BD38E8C44701D80DD96BF0121DA21843672E/"
|
||||
}
|
||||
|
||||
function onload()
|
||||
self.createButton({
|
||||
label="Add",
|
||||
click_function="addBlessToken",
|
||||
function_owner=self,
|
||||
position={-2.3,0.1,-0.5},
|
||||
height=150,
|
||||
width=300,
|
||||
scale={x=1.75, y=1.75, z=1.75},
|
||||
font_size=100,
|
||||
font_color={ r=1, g=1, b=1 },
|
||||
color=BLESS_COLOR
|
||||
})
|
||||
|
||||
self.createButton({
|
||||
label="Remove",
|
||||
click_function="removeBlessToken",
|
||||
function_owner=self,
|
||||
position={-0.9,0.1,-0.5},
|
||||
height=150,
|
||||
width=450,
|
||||
scale={x=1.75, y=1.75, z=1.75},
|
||||
font_size=100,
|
||||
font_color={ r=1, g=1, b=1 },
|
||||
color=BLESS_COLOR
|
||||
})
|
||||
|
||||
self.createButton({
|
||||
label="Take",
|
||||
click_function="takeBlessToken",
|
||||
function_owner=self,
|
||||
position={0.7,0.1,-0.5},
|
||||
height=150,
|
||||
width=350,
|
||||
scale={x=1.75, y=1.75, z=1.75},
|
||||
font_size=100,
|
||||
font_color={ r=1, g=1, b=1 },
|
||||
color=BLESS_COLOR
|
||||
})
|
||||
|
||||
self.createButton({
|
||||
label="Return",
|
||||
click_function="returnBlessToken",
|
||||
function_owner=self,
|
||||
position={2.1,0.1,-0.5},
|
||||
height=150,
|
||||
width=400,
|
||||
scale={x=1.75, y=1.75, z=1.75},
|
||||
font_size=100,
|
||||
font_color={ r=1, g=1, b=1 },
|
||||
color=BLESS_COLOR
|
||||
})
|
||||
|
||||
self.createButton({
|
||||
label="Add",
|
||||
click_function="addCurseToken",
|
||||
function_owner=self,
|
||||
position={-2.3,0.1,0.5},
|
||||
height=150,
|
||||
width=300,
|
||||
scale={x=1.75, y=1.75, z=1.75},
|
||||
font_size=100,
|
||||
font_color={ r=1, g=1, b=1 },
|
||||
color=CURSE_COLOR
|
||||
})
|
||||
|
||||
self.createButton({
|
||||
label="Remove",
|
||||
click_function="removeCurseToken",
|
||||
function_owner=self,
|
||||
position={-0.9,0.1,0.5},
|
||||
height=150,
|
||||
width=450,
|
||||
scale={x=1.75, y=1.75, z=1.75},
|
||||
font_size=100,
|
||||
font_color={ r=1, g=1, b=1 },
|
||||
color=CURSE_COLOR
|
||||
})
|
||||
|
||||
self.createButton({
|
||||
label="Take",
|
||||
click_function="takeCurseToken",
|
||||
function_owner=self,
|
||||
position={0.7,0.1,0.5},
|
||||
height=150,
|
||||
width=350,
|
||||
scale={x=1.75, y=1.75, z=1.75},
|
||||
font_size=100,
|
||||
font_color={ r=1, g=1, b=1 },
|
||||
color=CURSE_COLOR
|
||||
})
|
||||
|
||||
self.createButton({
|
||||
label="Return",
|
||||
click_function="returnCurseToken",
|
||||
function_owner=self,
|
||||
position={2.1,0.1,0.5},
|
||||
height=150,
|
||||
width=400,
|
||||
scale={x=1.75, y=1.75, z=1.75},
|
||||
font_size=100,
|
||||
font_color={ r=1, g=1, b=1 },
|
||||
color=CURSE_COLOR
|
||||
})
|
||||
|
||||
self.createButton({
|
||||
label="Reset", click_function="doReset", function_owner=self,
|
||||
position={0,0.3,1.8}, rotation={0,0,0}, height=350, width=800,
|
||||
font_size=250, color={0,0,0}, font_color={1,1,1}
|
||||
})
|
||||
|
||||
numInPlay = { Bless=0, Curse=0 }
|
||||
tokensTaken = { Bless={}, Curse={} }
|
||||
Wait.time(initializeState, 1)
|
||||
|
||||
addHotkey("Bless Curse Status", printStatus, false)
|
||||
end
|
||||
|
||||
function initializeState()
|
||||
playerColor = "White"
|
||||
-- count tokens in the bag
|
||||
local chaosbag = getChaosBag()
|
||||
if chaosbag == nil then return end
|
||||
local tokens = {}
|
||||
for i,v in ipairs(chaosbag.getObjects()) do
|
||||
if v.name == "Bless" then
|
||||
numInPlay.Bless = numInPlay.Bless + 1
|
||||
elseif v.name == "Curse" then
|
||||
numInPlay.Curse = numInPlay.Curse + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- find tokens in the play area
|
||||
local objs = Physics.cast({
|
||||
origin = { x=-33, y=0, z=0.5 },
|
||||
direction = { x=0, y=1, z=0 },
|
||||
type = 3,
|
||||
size = { x=77, y=5, z=77 },
|
||||
orientation = { x=0, y=90, z=0 }
|
||||
})
|
||||
|
||||
for i,v in ipairs(objs) do
|
||||
local obj = v.hit_object
|
||||
if obj.getName() == "Bless" then
|
||||
table.insert(tokensTaken.Bless, obj.getGUID())
|
||||
numInPlay.Bless = numInPlay.Bless + 1
|
||||
elseif obj.getName() == "Curse" then
|
||||
table.insert(tokensTaken.Curse, obj.getGUID())
|
||||
numInPlay.Curse = numInPlay.Curse + 1
|
||||
end
|
||||
end
|
||||
|
||||
mode = "Bless"
|
||||
print("Bless Tokens " .. getTokenCount())
|
||||
mode = "Curse"
|
||||
print("Curse Tokens " .. getTokenCount())
|
||||
end
|
||||
|
||||
function printStatus(player_color, hovered_object, world_position, key_down_up)
|
||||
mode = "Curse"
|
||||
broadcastToColor("Curse Tokens " .. getTokenCount(), player_color)
|
||||
mode = "Bless"
|
||||
broadcastToColor("Bless Tokens " .. getTokenCount(), player_color)
|
||||
end
|
||||
|
||||
function doReset(_obj, _color, alt_click)
|
||||
playerColor = _color
|
||||
numInPlay = { Bless=0, Curse=0 }
|
||||
tokensTaken = { Bless={}, Curse={} }
|
||||
initializeState()
|
||||
end
|
||||
|
||||
function addBlessToken(_obj, _color, alt_click)
|
||||
addToken("Bless", _color)
|
||||
end
|
||||
|
||||
function addCurseToken(_obj, _color, alt_click)
|
||||
addToken("Curse", _color)
|
||||
end
|
||||
|
||||
function addToken(type, _color)
|
||||
if numInPlay[type] == MAX_VALUE then
|
||||
printToColor(MAX_VALUE .. " tokens already in play, not adding any", _color)
|
||||
else
|
||||
mode = type
|
||||
spawnToken()
|
||||
end
|
||||
end
|
||||
|
||||
function spawnToken()
|
||||
local pos = getChaosBagPosition()
|
||||
if pos == nil then return end
|
||||
local url = IMAGE_URL[mode]
|
||||
local obj = spawnObject({
|
||||
type = 'Custom_Tile',
|
||||
position = {pos.x, pos.y + 3, pos.z},
|
||||
rotation = {x = 0, y = 260, z = 0},
|
||||
callback_function = spawn_callback
|
||||
})
|
||||
obj.setCustomObject({
|
||||
type = 2,
|
||||
image = url,
|
||||
thickness = 0.10,
|
||||
})
|
||||
obj.scale {0.81, 1, 0.81}
|
||||
return obj
|
||||
end
|
||||
|
||||
function spawn_callback(obj)
|
||||
obj.setName(mode)
|
||||
local guid = obj.getGUID()
|
||||
numInPlay[mode] = numInPlay[mode] + 1
|
||||
printToAll("Adding " .. mode .. " token " .. getTokenCount())
|
||||
end
|
||||
|
||||
function removeBlessToken(_obj, _color, alt_click)
|
||||
takeToken("Bless", _color, true)
|
||||
end
|
||||
|
||||
function removeCurseToken(_obj, _color, alt_click)
|
||||
takeToken("Curse", _color, true)
|
||||
end
|
||||
|
||||
function takeBlessToken(_obj, _color, alt_click)
|
||||
takeToken("Bless", _color, false)
|
||||
end
|
||||
|
||||
function takeCurseToken(_obj, _color, alt_click)
|
||||
takeToken("Curse", _color, false)
|
||||
end
|
||||
|
||||
function takeToken(type, _color, remove)
|
||||
playerColor = _color
|
||||
local chaosbag = getChaosBag()
|
||||
if chaosbag == nil then return end
|
||||
local tokens = {}
|
||||
for i,v in ipairs(chaosbag.getObjects()) do
|
||||
if v.name == type then
|
||||
table.insert(tokens, v.guid)
|
||||
end
|
||||
end
|
||||
if #tokens == 0 then
|
||||
printToColor("No " .. type .. " tokens in the chaos bag", _color)
|
||||
return
|
||||
end
|
||||
local pos = self.getPosition()
|
||||
local callback = take_callback
|
||||
if remove then
|
||||
callback = remove_callback
|
||||
num = removeNum
|
||||
end
|
||||
local guid = table.remove(tokens)
|
||||
mode = type
|
||||
chaosbag.takeObject({
|
||||
guid = guid,
|
||||
position = {pos.x-2, pos.y, pos.z},
|
||||
smooth = false,
|
||||
callback_function = callback
|
||||
})
|
||||
end
|
||||
|
||||
function remove_callback(obj)
|
||||
take_callback(obj, true)
|
||||
end
|
||||
|
||||
function take_callback(obj, remove)
|
||||
local guid = obj.getGUID()
|
||||
if remove then
|
||||
numInPlay[mode] = numInPlay[mode] - 1
|
||||
printToAll("Removing " .. mode .. " token " .. getTokenCount())
|
||||
obj.destruct()
|
||||
else
|
||||
table.insert(tokensTaken[mode], guid)
|
||||
printToAll("Taking " .. mode .. " token " .. getTokenCount())
|
||||
end
|
||||
end
|
||||
|
||||
function returnBlessToken(_obj, _color, alt_click)
|
||||
returnToken("Bless", _color)
|
||||
end
|
||||
|
||||
function returnCurseToken(_obj, _color, alt_click)
|
||||
returnToken("Curse", _color)
|
||||
end
|
||||
|
||||
function returnToken(type, _color)
|
||||
mode = type
|
||||
local guid = table.remove(tokensTaken[type])
|
||||
if guid == nil then
|
||||
printToColor("No " .. mode .. " tokens to return", _color)
|
||||
return
|
||||
end
|
||||
local token = getObjectFromGUID(guid)
|
||||
if token == nil then
|
||||
printToColor("Couldn't find token " .. guid .. ", not returning to bag", _color)
|
||||
return
|
||||
end
|
||||
playerColor = _color
|
||||
local chaosbag = getChaosBag()
|
||||
if chaosbag == nil then return end
|
||||
chaosbag.putObject(token)
|
||||
printToAll("Returning " .. type .. " token " .. getTokenCount())
|
||||
end
|
||||
|
||||
function getChaosBag()
|
||||
local items = getObjectFromGUID("83ef06").getObjects()
|
||||
local chaosbag = nil
|
||||
for i,v in ipairs(items) do
|
||||
if v.getDescription() == "Chaos Bag" then
|
||||
chaosbag = getObjectFromGUID(v.getGUID())
|
||||
break
|
||||
end
|
||||
end
|
||||
if chaosbag == nil then printToColor("No chaos bag found", playerColor) end
|
||||
return chaosbag
|
||||
end
|
||||
|
||||
function getChaosBagPosition()
|
||||
local chaosbag = getChaosBag()
|
||||
if chaosbag == nil then return nil end
|
||||
return chaosbag.getPosition()
|
||||
end
|
||||
|
||||
function getTokenCount()
|
||||
return "(" .. (numInPlay[mode] - #tokensTaken[mode]) .. "/" ..
|
||||
#tokensTaken[mode] .. ")"
|
||||
end
|
273
src/util/ClueCounterSwapper.ttslua
Normal file
273
src/util/ClueCounterSwapper.ttslua
Normal file
@ -0,0 +1,273 @@
|
||||
function updateSave()
|
||||
local data_to_save = {["ml"]=memoryList}
|
||||
saved_data = JSON.encode(data_to_save)
|
||||
self.script_state = saved_data
|
||||
end
|
||||
|
||||
function onload(saved_data)
|
||||
if saved_data ~= "" then
|
||||
local loaded_data = JSON.decode(saved_data)
|
||||
--Set up information off of loaded_data
|
||||
memoryList = loaded_data.ml
|
||||
else
|
||||
--Set up information for if there is no saved saved data
|
||||
memoryList = {}
|
||||
end
|
||||
|
||||
if next(memoryList) == nil then
|
||||
createSetupButton()
|
||||
else
|
||||
createMemoryActionButtons()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--Beginning Setup
|
||||
|
||||
|
||||
--Make setup button
|
||||
function createSetupButton()
|
||||
self.createButton({
|
||||
label="Setup", click_function="buttonClick_setup", function_owner=self,
|
||||
position={0,5,-2}, rotation={0,0,0}, height=250, width=600,
|
||||
font_size=150, color={0,0,0}, font_color={1,1,1}
|
||||
})
|
||||
end
|
||||
|
||||
--Triggered by setup button,
|
||||
function buttonClick_setup()
|
||||
memoryListBackup = duplicateTable(memoryList)
|
||||
memoryList = {}
|
||||
self.clearButtons()
|
||||
createButtonsOnAllObjects()
|
||||
createSetupActionButtons()
|
||||
end
|
||||
|
||||
--Creates selection buttons on objects
|
||||
function createButtonsOnAllObjects()
|
||||
local howManyButtons = 0
|
||||
for _, obj in ipairs(getAllObjects()) do
|
||||
if obj ~= self then
|
||||
local dummyIndex = howManyButtons
|
||||
--On a normal bag, the button positions aren't the same size as the bag.
|
||||
globalScaleFactor = 1.25 * 1/self.getScale().x
|
||||
--Super sweet math to set button positions
|
||||
local selfPos = self.getPosition()
|
||||
local objPos = obj.getPosition()
|
||||
local deltaPos = findOffsetDistance(selfPos, objPos, obj)
|
||||
local objPos = rotateLocalCoordinates(deltaPos, self)
|
||||
objPos.x = -objPos.x * globalScaleFactor
|
||||
objPos.y = objPos.y * globalScaleFactor
|
||||
objPos.z = objPos.z * 4
|
||||
--Offset rotation of bag
|
||||
local rot = self.getRotation()
|
||||
rot.y = -rot.y + 180
|
||||
--Create function
|
||||
local funcName = "selectButton_" .. howManyButtons
|
||||
local func = function() buttonClick_selection(dummyIndex, obj) end
|
||||
self.setVar(funcName, func)
|
||||
self.createButton({
|
||||
click_function=funcName, function_owner=self,
|
||||
position=objPos, rotation=rot, height=1000, width=1000,
|
||||
color={0.75,0.25,0.25,0.6},
|
||||
})
|
||||
howManyButtons = howManyButtons + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--Creates submit and cancel buttons
|
||||
function createSetupActionButtons()
|
||||
self.createButton({
|
||||
label="Cancel", click_function="buttonClick_cancel", function_owner=self,
|
||||
position={1.5,5,2}, rotation={0,0,0}, height=350, width=1100,
|
||||
font_size=250, color={0,0,0}, font_color={1,1,1}
|
||||
})
|
||||
self.createButton({
|
||||
label="Submit", click_function="buttonClick_submit", function_owner=self,
|
||||
position={-1.2,5,2}, rotation={0,0,0}, height=350, width=1100,
|
||||
font_size=250, color={0,0,0}, font_color={1,1,1}
|
||||
})
|
||||
self.createButton({
|
||||
label="Reset", click_function="buttonClick_reset", function_owner=self,
|
||||
position={-3.5,5,2}, rotation={0,0,0}, height=350, width=800,
|
||||
font_size=250, color={0,0,0}, font_color={1,1,1}
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
--During Setup
|
||||
|
||||
|
||||
--Checks or unchecks buttons
|
||||
function buttonClick_selection(index, obj)
|
||||
local color = {0,1,0,0.6}
|
||||
if memoryList[obj.getGUID()] == nil then
|
||||
self.editButton({index=index, color=color})
|
||||
--Adding pos/rot to memory table
|
||||
local pos, rot = obj.getPosition(), obj.getRotation()
|
||||
--I need to add it like this or it won't save due to indexing issue
|
||||
memoryList[obj.getGUID()] = {
|
||||
pos={x=round(pos.x,4), y=round(pos.y,4), z=round(pos.z,4)},
|
||||
rot={x=round(rot.x,4), y=round(rot.y,4), z=round(rot.z,4)},
|
||||
lock=obj.getLock()
|
||||
}
|
||||
obj.highlightOn({0,1,0})
|
||||
else
|
||||
color = {0.75,0.25,0.25,0.6}
|
||||
self.editButton({index=index, color=color})
|
||||
memoryList[obj.getGUID()] = nil
|
||||
obj.highlightOff()
|
||||
end
|
||||
end
|
||||
|
||||
--Cancels selection process
|
||||
function buttonClick_cancel()
|
||||
memoryList = memoryListBackup
|
||||
self.clearButtons()
|
||||
if next(memoryList) == nil then
|
||||
createSetupButton()
|
||||
else
|
||||
createMemoryActionButtons()
|
||||
end
|
||||
removeAllHighlights()
|
||||
broadcastToAll("Selection Canceled", {1,1,1})
|
||||
end
|
||||
|
||||
--Saves selections
|
||||
function buttonClick_submit()
|
||||
if next(memoryList) == nil then
|
||||
broadcastToAll("You cannot submit without any selections.", {0.75, 0.25, 0.25})
|
||||
else
|
||||
self.clearButtons()
|
||||
createMemoryActionButtons()
|
||||
local count = 0
|
||||
for guid in pairs(memoryList) do
|
||||
count = count + 1
|
||||
local obj = getObjectFromGUID(guid)
|
||||
if obj ~= nil then obj.highlightOff() end
|
||||
end
|
||||
broadcastToAll(count.." Objects Saved", {1,1,1})
|
||||
updateSave()
|
||||
end
|
||||
end
|
||||
|
||||
--Resets bag to starting status
|
||||
function buttonClick_reset()
|
||||
memoryList = {}
|
||||
self.clearButtons()
|
||||
createSetupButton()
|
||||
removeAllHighlights()
|
||||
broadcastToAll("Tool Reset", {1,1,1})
|
||||
updateSave()
|
||||
end
|
||||
|
||||
|
||||
--After Setup
|
||||
|
||||
|
||||
--Creates recall and place buttons
|
||||
function createMemoryActionButtons()
|
||||
self.createButton({
|
||||
label="Clicker", click_function="buttonClick_place", function_owner=self,
|
||||
position={4.2,1,0}, rotation={0,0,0}, height=500, width=1100,
|
||||
font_size=350, color={0,0,0}, font_color={1,1,1}
|
||||
})
|
||||
self.createButton({
|
||||
label="Counter", click_function="buttonClick_recall", function_owner=self,
|
||||
position={-4.2,1,-0.1}, rotation={0,0,0}, height=500, width=1300,
|
||||
font_size=350, color={0,0,0}, font_color={1,1,1}
|
||||
})
|
||||
-- self.createButton({
|
||||
-- label="Setup", click_function="buttonClick_setup", function_owner=self,
|
||||
-- position={-6,1,0}, rotation={0,90,0}, height=500, width=1200,
|
||||
-- font_size=350, color={0,0,0}, font_color={1,1,1}
|
||||
-- })
|
||||
end
|
||||
|
||||
--Sends objects from bag/table to their saved position/rotation
|
||||
function buttonClick_place()
|
||||
local bagObjList = self.getObjects()
|
||||
for guid, entry in pairs(memoryList) do
|
||||
local obj = getObjectFromGUID(guid)
|
||||
--If obj is out on the table, move it to the saved pos/rot
|
||||
if obj ~= nil then
|
||||
obj.setPositionSmooth(entry.pos)
|
||||
obj.setRotationSmooth(entry.rot)
|
||||
obj.setLock(entry.lock)
|
||||
else
|
||||
--If obj is inside of the bag
|
||||
for _, bagObj in ipairs(bagObjList) do
|
||||
if bagObj.guid == guid then
|
||||
local item = self.takeObject({
|
||||
guid=guid, position=entry.pos, rotation=entry.rot,
|
||||
})
|
||||
item.setLock(entry.lock)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
broadcastToAll("Objects Placed", {1,1,1})
|
||||
end
|
||||
|
||||
--Recalls objects to bag from table
|
||||
function buttonClick_recall()
|
||||
for guid, entry in pairs(memoryList) do
|
||||
local obj = getObjectFromGUID(guid)
|
||||
if obj ~= nil then self.putObject(obj) end
|
||||
end
|
||||
broadcastToAll("Objects Recalled", {1,1,1})
|
||||
end
|
||||
|
||||
|
||||
--Utility functions
|
||||
|
||||
|
||||
--Find delta (difference) between 2 x/y/z coordinates
|
||||
function findOffsetDistance(p1, p2, obj)
|
||||
local deltaPos = {}
|
||||
local bounds = obj.getBounds()
|
||||
deltaPos.x = (p2.x-p1.x)
|
||||
deltaPos.y = (p2.y-p1.y) + (bounds.size.y - bounds.offset.y)
|
||||
deltaPos.z = (p2.z-p1.z)
|
||||
return deltaPos
|
||||
end
|
||||
|
||||
--Used to rotate a set of coordinates by an angle
|
||||
function rotateLocalCoordinates(desiredPos, obj)
|
||||
local objPos, objRot = obj.getPosition(), obj.getRotation()
|
||||
local angle = math.rad(objRot.y)
|
||||
local x = desiredPos.x * math.cos(angle) - desiredPos.z * math.sin(angle)
|
||||
local z = desiredPos.x * math.sin(angle) + desiredPos.z * math.cos(angle)
|
||||
--return {x=objPos.x+x, y=objPos.y+desiredPos.y, z=objPos.z+z}
|
||||
return {x=x, y=desiredPos.y, z=z}
|
||||
end
|
||||
|
||||
--Coroutine delay, in seconds
|
||||
function wait(time)
|
||||
local start = os.time()
|
||||
repeat coroutine.yield(0) until os.time() > start + time
|
||||
end
|
||||
|
||||
--Duplicates a table (needed to prevent it making reference to the same objects)
|
||||
function duplicateTable(oldTable)
|
||||
local newTable = {}
|
||||
for k, v in pairs(oldTable) do
|
||||
newTable[k] = v
|
||||
end
|
||||
return newTable
|
||||
end
|
||||
|
||||
--Moves scripted highlight from all objects
|
||||
function removeAllHighlights()
|
||||
for _, obj in ipairs(getAllObjects()) do
|
||||
obj.highlightOff()
|
||||
end
|
||||
end
|
||||
|
||||
--Round number (num) to the Nth decimal (dec)
|
||||
function round(num, dec)
|
||||
local mult = 10^(dec or 0)
|
||||
return math.floor(num * mult + 0.5) / mult
|
||||
end
|
71
src/util/HandSizeCounter.ttslua
Normal file
71
src/util/HandSizeCounter.ttslua
Normal file
@ -0,0 +1,71 @@
|
||||
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
|
13
src/util/HandSizeCounter.xml
Normal file
13
src/util/HandSizeCounter.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<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>
|
0
src/util/StatTracker.ttslua
Normal file
0
src/util/StatTracker.ttslua
Normal file
72
src/util/TokenRemover.ttslua
Normal file
72
src/util/TokenRemover.ttslua
Normal file
@ -0,0 +1,72 @@
|
||||
---
|
||||
--- Generated by EmmyLua(https://github.com/EmmyLua)
|
||||
--- Created by Whimsical.
|
||||
--- DateTime: 2021-02-02 9:41 a.m.
|
||||
---
|
||||
|
||||
local zone = nil
|
||||
|
||||
-- Forward Declaration
|
||||
---@param is_enabled boolean
|
||||
local setMenu = function(is_enabled) end
|
||||
|
||||
local function enable()
|
||||
if self.held_by_color~=nil then return end
|
||||
local position = self:getPosition()
|
||||
local rotation = self:getRotation()
|
||||
local scale = self:getScale()
|
||||
|
||||
zone = spawnObject {
|
||||
type = "ScriptingTrigger",
|
||||
position = Vector(position.x, position.y+25+(bit32.rshift(scale.y, 1))+0.41, position.z),
|
||||
rotation = rotation,
|
||||
scale = Vector(scale.x*2, 50, scale.z*2),
|
||||
sound = true,
|
||||
snap_to_grid = true
|
||||
}
|
||||
|
||||
setMenu(false)
|
||||
end
|
||||
|
||||
local function disable()
|
||||
if zone~=nil then zone:destruct() end
|
||||
setMenu(true)
|
||||
end
|
||||
|
||||
---@param is_enabled boolean
|
||||
setMenu = function(is_enabled)
|
||||
self:clearContextMenu()
|
||||
if is_enabled then
|
||||
self:addContextMenuItem("Enable", enable, false)
|
||||
else
|
||||
self:addContextMenuItem("Disable", disable, false)
|
||||
end
|
||||
end
|
||||
|
||||
function onLoad(save_state)
|
||||
if save_state=="" then return end
|
||||
local data = JSON.decode(save_state)
|
||||
zone = getObjectFromGUID(data.zone)
|
||||
setMenu(zone==nil)
|
||||
end
|
||||
|
||||
function onSave()
|
||||
return JSON.encode {
|
||||
zone = zone and zone:getGUID() or nil
|
||||
}
|
||||
end
|
||||
|
||||
---@param entering TTSObject
|
||||
---@param object TTSObject
|
||||
function onObjectEnterScriptingZone(entering , object)
|
||||
if zone~=entering then return end
|
||||
if object==self then return end
|
||||
if object.type=="Deck" or object.type=="Card" then return end
|
||||
|
||||
object:destruct()
|
||||
end
|
||||
|
||||
---@param color string
|
||||
function onPickUp(color)
|
||||
disable()
|
||||
end
|
92
src/util/TokenSpawner.ttslua
Normal file
92
src/util/TokenSpawner.ttslua
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user