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:
Kevin 2021-10-06 20:37:31 -07:00
parent 852e384532
commit 83f75c2318
40 changed files with 10049 additions and 0 deletions

View 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

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

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

View 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

View File

@ -0,0 +1,103 @@
function onload(saved_data)
light_mode = false
if saved_data ~= "" then
local loaded_data = JSON.decode(saved_data)
light_mode = loaded_data[1]
end
createAll()
end
-- functions delegated to Global
function printStats(object, player, isRightClick)
-- local toPosition = self.positionToWorld(DRAWN_CHAOS_TOKEN_OFFSET)
if isRightClick then
Global.call("resetStats")
else
Global.call("printStats")
end
end
function updateSave()
local data_to_save = {light_mode }
saved_data = JSON.encode(data_to_save)
self.script_state = saved_data
end
function createAll()
s_color = {0.5, 0.5, 0.5, 95}
if light_mode then
f_color = {1,1,1,95}
else
f_color = {0,0,0,100}
end
self.createButton({
click_function="printStats",
function_owner=self,
position={0,0.05,0},
height=600,
width=1000,
alignment = 3,
tooltip = "Left Click to print stats. Right Click to reset them.",
scale={x=1.5, y=1.5, z=1.5},
font_size=600,
font_color=f_color,
color={0,0,0,0}
})
if light_mode then
lightButtonText = "[ Set dark ]"
else
lightButtonText = "[ Set light ]"
end
end
function removeAll()
self.removeInput(0)
self.removeInput(1)
self.removeButton(0)
self.removeButton(1)
self.removeButton(2)
end
function reloadAll()
removeAll()
createAll()
updateSave()
end
function swap_fcolor(_obj, _color, alt_click)
light_mode = not light_mode
reloadAll()
end
function swap_align(_obj, _color, alt_click)
center_mode = not center_mode
reloadAll()
end
function editName(_obj, _string, value)
self.setName(value)
setTooltips()
end
function setTooltips()
self.editInput({
index = 0,
value = self.getName(),
tooltip = "Left click to show stats. Right click to reset them."
})
end
function keepSample(_obj, _string, value)
reloadAll()
end
function onDestroy()
if timerID and type(timerID) == 'object' then
Timer.destroy(timerID)
end
end

View File

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

@ -0,0 +1,132 @@
MIN_VALUE = -99
MAX_VALUE = 999
function onload(saved_data)
light_mode = false
val = 0
if saved_data ~= "" then
local loaded_data = JSON.decode(saved_data)
light_mode = loaded_data[1]
val = loaded_data[2]
end
createAll()
end
function updateSave()
local data_to_save = {light_mode, val}
saved_data = JSON.encode(data_to_save)
self.script_state = saved_data
end
function createAll()
s_color = {0.5, 0.5, 0.5, 95}
if light_mode then
f_color = {1,1,1,95}
else
f_color = {0,0,0,100}
end
self.createButton({
label=tostring(val),
click_function="add_subtract",
function_owner=self,
position={0,0.05,0},
height=600,
width=1000,
alignment = 3,
scale={x=1.5, y=1.5, z=1.5},
font_size=600,
font_color=f_color,
color={0,0,0,0}
})
if light_mode then
lightButtonText = "[ Set dark ]"
else
lightButtonText = "[ Set light ]"
end
end
function removeAll()
self.removeInput(0)
self.removeInput(1)
self.removeButton(0)
self.removeButton(1)
self.removeButton(2)
end
function reloadAll()
removeAll()
createAll()
updateSave()
end
function swap_fcolor(_obj, _color, alt_click)
light_mode = not light_mode
reloadAll()
end
function swap_align(_obj, _color, alt_click)
center_mode = not center_mode
reloadAll()
end
function editName(_obj, _string, value)
self.setName(value)
setTooltips()
end
function add_subtract(_obj, _color, alt_click)
mod = alt_click and -1 or 1
new_value = math.min(math.max(val + mod, MIN_VALUE), MAX_VALUE)
if val ~= new_value then
val = new_value
updateVal()
updateSave()
end
end
function updateVal()
self.editButton({
index = 0,
label = tostring(val),
})
end
function reset_val()
val = 0
updateVal()
updateSave()
end
function setTooltips()
self.editInput({
index = 0,
value = self.getName(),
tooltip = ttText
})
self.editButton({
index = 0,
value = tostring(val),
tooltip = ttText
})
end
function null()
end
function keepSample(_obj, _string, value)
reloadAll()
end

View File

1979
src/core/DataHelper.ttslua Normal file

File diff suppressed because it is too large Load Diff

768
src/core/Global.ttslua Normal file
View 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

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

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

View File

View 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

File diff suppressed because one or more lines are too long