ah_sce_unpacked/unpacked/Custom_Tile Playermat 4 Red 0840d5.ttslua

702 lines
23 KiB
Plaintext

-- Bundled by luabundle {"version":"1.6.0"}
local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)
local loadingPlaceholder = {[{}] = true}
local register
local modules = {}
local require
local loaded = {}
register = function(name, body)
if not modules[name] then
modules[name] = body
end
end
require = function(name)
local loadedModule = loaded[name]
if loadedModule then
if loadedModule == loadingPlaceholder then
return nil
end
else
if not modules[name] then
if not superRequire then
local identifier = type(name) == 'string' and '\"' .. name .. '\"' or tostring(name)
error('Tried to require ' .. identifier .. ', but no such module has been registered')
else
return superRequire(name)
end
end
loaded[name] = loadingPlaceholder
loadedModule = modules[name](require, loaded, register, modules)
loaded[name] = loadedModule
end
return loadedModule
end
return require, loaded, register, modules
end)(nil)
__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
---------------------------------------------------------
-- specific setup (different for each playmat)
---------------------------------------------------------
PLAYER_COLOR = "Red"
PLAY_ZONE_POSITION = { x = -25, y = 4, z = -27 }
PLAY_ZONE_SCALE = { x = 30, y = 5, z = 14.5 }
DRAW_DECK_POSITION = { x = -13.78, y = 2.5, z = -26.37 }
DISCARD_PILE_POSITION = { x = -13.78, y = 4, z = -30.48 }
TRASHCAN_GUID = "4b8594"
STAT_TRACKER_GUID = "e74881"
RESOURCE_COUNTER_GUID = "a4b60d"
require("playermat/Playmat")
end)
__bundle_register("playermat/Playmat", function(require, _LOADED, __bundle_register, __bundle_modules)
-- set true to enable debug logging and show Physics.cast()
local DEBUG = false
-- we use this to turn off collision handling until onLoad() is complete
local COLLISION_ENABLED = false
-- position offsets relative to mat [x, y, z]
local DRAWN_ENCOUNTER_CARD_OFFSET = {1.365, 0.5, -0.635}
local DRAWN_CHAOS_TOKEN_OFFSET = {-1.55, 0.5, -0.58}
local DISCARD_BUTTON_OFFSETS = {
{-1.365, 0.1, -0.945},
{-0.91, 0.1, -0.945},
{-0.455, 0.1, -0.945},
{0, 0.1, -0.945},
{0.455, 0.1, -0.945},
{0.91, 0.1, -0.945}
}
local PLAY_ZONE_ROTATION = self.getRotation()
local TRASHCAN
local STAT_TRACKER
local RESOURCE_COUNTER
-- global variable so it can be reset by the Clean Up Helper
activeInvestigatorId = "00000"
function onSave() return JSON.encode({zoneID = zoneID, playerColor = PLAYER_COLOR, activeInvestigatorId = activeInvestigatorId}) 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')
TRASHCAN = getObjectFromGUID(TRASHCAN_GUID)
STAT_TRACKER = getObjectFromGUID(STAT_TRACKER_GUID)
RESOURCE_COUNTER = getObjectFromGUID(RESOURCE_COUNTER_GUID)
for i = 1, 6 do
makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], {-3.85, 3, 10.38}, i)
end
self.createButton({
click_function = "drawEncountercard",
function_owner = self,
position = {-1.84, 0, -0.65},
rotation = {0, 80, 0},
width = 265,
height = 190
})
self.createButton({
click_function = "drawChaostokenButton",
function_owner = self,
position = {1.85, 0, -0.74},
rotation = {0, -45, 0},
width = 135,
height = 135
})
self.createButton({
label = "Upkeep",
click_function = "doUpkeep",
function_owner = self,
position = {1.84, 0.1, -0.44},
scale = {0.12, 0.12, 0.12},
width = 800,
height = 280,
font_size = 180
})
local state = JSON.decode(save_state)
if state ~= nil then
zoneID = state.zoneID
PLAYER_COLOR = state.playerColor
activeInvestigatorId = state.activeInvestigatorId
end
if getObjectFromGUID(zoneID) == nil then spawnDeckZone() end
COLLISION_ENABLED = true
end
---------------------------------------------------------
-- utility functions
---------------------------------------------------------
function log(message)
if DEBUG then print(message) end
end
-- send messages to player who clicked button if no seated player found
function setMessageColor(color)
messageColor = Player[PLAYER_COLOR].seated and PLAYER_COLOR or color
end
function spawnDeckZone()
spawnObject({
position = self.positionToWorld({-1.4, 0, 0.3 }),
scale = {3, 5, 8 },
type = 'ScriptingTrigger',
callback = function (zone) zoneID = zone.getGUID() end,
callback_owner = self,
rotation = self.getRotation()
})
end
function searchArea(origin, size)
return Physics.cast({
origin = origin,
direction = {0, 1, 0},
orientation = PLAY_ZONE_ROTATION,
type = 3,
size = size,
max_distance = 1,
debug = DEBUG
})
end
function doNotReady(card) return card.getVar("do_not_ready") or false end
---------------------------------------------------------
-- Discard buttons
---------------------------------------------------------
-- builds a function that discards things in searchPosition to discardPosition
-- stuff on the card/deck will be put into the local trashcan
function makeDiscardHandlerFor(searchPosition, discardPosition)
return function ()
for _, hitObj in ipairs(findObjectsAtPosition(searchPosition)) do
local obj = hitObj.hit_object
if obj.tag == "Deck" or obj.tag == "Card" then
if obj.hasTag("PlayerCard") then
obj.setPositionSmooth(DISCARD_PILE_POSITION, false, true)
obj.setRotation(PLAY_ZONE_ROTATION)
else
obj.setPositionSmooth(discardPosition, false, true)
obj.setRotation({0, -90, 0})
end
-- don't touch the table or this playmat itself
elseif obj.guid ~= "4ee1f2" and obj ~= self then
TRASHCAN.putObject(obj)
end
end
end
end
-- build a discard button to discard from searchPosition to discardPosition (number must be unique)
function makeDiscardButton(position, discardPosition, number)
local searchPosition = {-position[1], position[2], position[3] + 0.32}
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 findObjectsAtPosition(localPos)
return Physics.cast({
origin = self.positionToWorld(localPos),
direction = {0, 1, 0},
orientation = {0, PLAY_ZONE_ROTATION.y + 90, 0},
type = 3,
size = {3.2, 1, 2},
max_distance = 0,
debug = DEBUG
})
end
---------------------------------------------------------
-- Upkeep button
---------------------------------------------------------
function doUpkeep(_, color, alt_click)
setMessageColor(color)
-- right-click binds to new player color
if alt_click then
PLAYER_COLOR = color
printToColor("Upkeep button bound to " .. color, color)
return
end
local forcedLearning = false
-- unexhaust cards in play zone, flip action tokens and find forcedLearning
for _, v in ipairs(searchArea(PLAY_ZONE_POSITION, PLAY_ZONE_SCALE)) do
local obj = v.hit_object
if obj.getDescription() == "Action Token" and obj.is_face_down then
obj.flip()
elseif obj.tag == "Card" and not obj.is_face_down then
local cardMetadata = JSON.decode(obj.getGMNotes()) or {}
if not doNotReady(obj) and cardMetadata.type ~= "Investigator" then
obj.setRotation(PLAY_ZONE_ROTATION)
end
if cardMetadata.id == "08031" then
forcedLearning = true
end
if cardMetadata.uses ~= nil then
-- check for cards with 'replenish' in their metadata
local count
local replenish
-- Uses structure underwent a breaking change in 2.4.0, have to check to see if this is
-- a single entry or an array. TODO: Clean this up when 2.4.0 has been out long
-- enough that saved decks don't have old data
if cardMetadata.uses.count ~= nil then
count = cardMetadata.uses.count
replenish = cardMetadata.uses.replenish
else
count = cardMetadata.uses[1].count
replenish = cardMetadata.uses[1].replenish
end
if count and replenish then
replenishTokens(obj, count, replenish)
end
end
end
end
-- flip investigator mini-card if found
-- flip summoned servitor mini-cards (To-Do: don't flip all of them)
if activeInvestigatorId ~= nil then
local miniId = string.match(activeInvestigatorId, "%d%d%d%d%d") .. "-m"
for _, obj in ipairs(getObjects()) do
if obj.tag == "Card" and obj.is_face_down then
local notes = JSON.decode(obj.getGMNotes())
if notes ~= nil and notes.type == "Minicard" and (notes.id == miniId or notes.id == "09080-m") then
obj.flip()
end
end
end
end
-- gain a resource
RESOURCE_COUNTER.call("addOrSubtract")
-- gain an additional resource for Jenny Barnes
if string.match(activeInvestigatorId, "%d%d%d%d%d") == "02003" then
RESOURCE_COUNTER.call("addOrSubtract")
printToColor("Gaining 2 resources (Jenny)", messageColor)
end
-- draw a card (with handling for Patrice and Forced Learning)
if activeInvestigatorId == "06005" then
if forcedLearning then
printToColor("Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'? Choose which draw replacement effect takes priority and draw cards accordingly.", messageColor)
else
local handSize = #Player[PLAYER_COLOR].getHandObjects()
if handSize < 5 then
local cardsToDraw = 5 - handSize
printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor)
drawCardsWithReshuffle(cardsToDraw)
end
end
elseif forcedLearning then
printToColor("Drawing 2 cards, discard 1 (Forced Learning)", messageColor)
drawCardsWithReshuffle(2)
else
drawCardsWithReshuffle(1)
end
end
-- legacy function for "add draw 1 button"
function doDrawOne(_, color)
setMessageColor(color)
drawCardsWithReshuffle(1)
end
-- draw X cards (shuffle discards if necessary)
function drawCardsWithReshuffle(numCards)
getDrawDiscardDecks()
-- Norman Withers handling
if string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then
local harbinger = false
if topCard ~= nil and topCard.getName() == "The Harbinger" then harbinger = true
elseif drawDeck ~= nil and not drawDeck.is_face_down then
local cards = drawDeck.getObjects()
if cards[#cards].name == "The Harbinger" then harbinger = true end
end
if harbinger then
printToColor("The Harbinger is on top of your deck, not drawing cards", messageColor)
return
end
if topCard ~= nil then
topCard.deal(numCards, PLAYER_COLOR)
numCards = numCards - 1
if numCards == 0 then return end
end
end
local deckSize = 1
if drawDeck == nil then
deckSize = 0
elseif drawDeck.tag == "Deck" then
deckSize = #drawDeck.getObjects()
end
if deckSize >= numCards then
drawCards(numCards)
return
end
drawCards(deckSize)
if discardPile ~= nil then
shuffleDiscardIntoDeck()
Wait.time(|| drawCards(numCards - deckSize), 1)
end
printToColor("Take 1 horror (drawing card from empty deck)", messageColor)
end
-- get the draw deck and discard pile objects
function getDrawDiscardDecks()
drawDeck = nil
discardPile = nil
topCard = nil
local zone = getObjectFromGUID(zoneID)
if zone == nil then return end
for _, object in ipairs(zone.getObjects()) do
if object.tag == "Deck" or object.tag == "Card" then
if self.positionToLocal(object.getPosition()).z > 0.5 then
discardPile = object
-- Norman Withers handling
elseif string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" and object.tag == "Card" and not object.is_face_down then
topCard = object
else
drawDeck = object
end
end
end
end
function drawCards(numCards)
if drawDeck == nil then return end
drawDeck.deal(numCards, PLAYER_COLOR)
end
function shuffleDiscardIntoDeck()
if not discardPile.is_face_down then discardPile.flip() end
discardPile.shuffle()
discardPile.setPositionSmooth(DRAW_DECK_POSITION, false, false)
drawDeck = discardPile
discardPile = nil
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
-- @param object Object to spawn the tokens on
-- @param tokenType Type of token to be spawned
-- @param tokenCount Number of tokens to spawn
-- @param shiftDown Amount to shift this group down to avoid spawning multiple token groups on
-- top of each other. Negative values are allowed, and will move the group up instead. This is
-- a static value and is unaware of how many tokens were spawned previously; callers should
-- ensure the proper shift.
function spawnTokenGroup(object, tokenType, tokenCount, shiftDown)
if (tokenCount < 1 or tokenCount > 12) then return end
local offsets = PLAYER_CARD_TOKEN_OFFSETS[tokenCount]
if shiftDown ~= nil then
-- Copy the offsets to make sure we don't change the static values
local baseOffsets = offsets
offsets = { }
for i, baseOffset in ipairs(baseOffsets) do
offsets[i] = baseOffset
offsets[i][3] = offsets[i][3] + shiftDown
end
end
if offsets == nil then error("couldn't find offsets for " .. tokenCount .. ' tokens') end
for i = 1, tokenCount do
spawnTokenOn(object, offsets[i], tokenType)
end
end
---------------------------------------------------------
-- playmat token spawning
---------------------------------------------------------
-- replenish Tokens for specific cards (like 'Physical Training (4)')
function replenishTokens(card, count, replenish)
local cardPos = card.getPosition()
-- don't continue for cards on your deck (Norman) or in your discard pile
if self.positionToLocal(cardPos).x < -1 then return end
-- get current amount of resource tokens on the card
local search = searchArea(cardPos, { 2.5, 0.5, 3.5 })
local foundTokens = 0
for _, obj in ipairs(search) do
local obj = obj.hit_object
if obj.getCustomObject().image == "http://cloud-3.steamusercontent.com/ugc/1758068501357192910/11DDDC7EF621320962FDCF3AE3211D5EDC3D1573/" then
foundTokens = foundTokens + math.abs(obj.getQuantity())
obj.destruct()
end
end
-- handling Runic Axe upgrade sheet for additional replenish
if card.getName() == "Runic Axe" then
for _, v in ipairs(searchArea(PLAY_ZONE_POSITION, PLAY_ZONE_SCALE)) do
local obj = v.hit_object
if obj.tag == "Card" then
local notes = JSON.decode(obj.getGMNotes()) or {}
if notes ~= nil and notes.id == "09022-c" then
if obj.getVar("markedBoxes")[7] == 3 then replenish = 2 end
break
end
end
end
end
local newCount = foundTokens + replenish
if newCount > count then newCount = count end
spawnTokenGroup(card, "resource", newCount)
end
function getPlayerCardData(object)
return PLAYER_CARDS[object.getName()..':'..object.getDescription()] or PLAYER_CARDS[object.getName()]
end
function shouldSpawnTokens(object)
-- don't spawn tokens if in doubt, this should only ever happen onLoad and prevents respawns
local spawned = DATA_HELPER.call('getSpawnedPlayerCardGuid', {object.getGUID()})
local hasDataHelperData = getPlayerCardData(object)
local cardMetadata = JSON.decode(object.getGMNotes()) or {}
local hasUses = cardMetadata.uses ~= nil
return not spawned and (hasDataHelperData or hasUses)
end
function markSpawned(object)
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
-- contains position and amount of boxes for the upgradesheets that change uses
-- Alchemical Distillation, Damning Testimony, Living Ink and Hyperphysical Shotcaster
local customizationsTable = {
["09040"] = {5, 2},
["09059"] = {2, 2},
["09079"] = {3, 2},
["09119"] = {6, 4}
}
function spawnTokensFor(object)
local cardMetadata = JSON.decode(object.getGMNotes()) or {}
local type = nil
local token = nil
local tokenCount = 0
if cardMetadata.uses ~= nil then
-- Uses structure underwent a breaking change in 2.4.0, have to check to see if this is
-- a single entry or an array. This is ugly and duplicated, but impossible to replicate the
-- multi-spawn vs. single spawn otherwise. TODO: Clean this up when 2.4.0 has been out long
-- enough that saved decks don't have old data
if cardMetadata.uses.count != nil then
type = cardMetadata.uses.type
token = cardMetadata.uses.token
tokenCount = cardMetadata.uses.count
if activeInvestigatorId == "03004" and type == "Charge" then tokenCount = tokenCount + 1 end
log("Spawning tokens for "..object.getName()..'['..object.getDescription()..']: '..tokenCount.."x "..token)
spawnTokenGroup(object, token, tokenCount)
else
for i, useInfo in ipairs(cardMetadata.uses) do
type = useInfo.type
token = useInfo.token
tokenCount = useInfo.count
-- additional uses for certain customizable cards (by checking the upgradesheets)
if customizationsTable[cardMetadata.id] ~= nil then
for _, obj in ipairs(searchArea(PLAY_ZONE_POSITION, PLAY_ZONE_SCALE)) do
local obj = obj.hit_object
if obj.tag == "Card" then
local notes = JSON.decode(obj.getGMNotes()) or {}
if notes.id == (cardMetadata.id .. "-c") then
local pos = customizationsTable[cardMetadata.id][1]
local boxes = customizationsTable[cardMetadata.id][2]
if obj.getVar("markedBoxes")[pos] == boxes then tokenCount = tokenCount + 2 end
break
end
end
end
end
-- additional charge for Akachi
if activeInvestigatorId == "03004" and type == "Charge" then tokenCount = tokenCount + 1 end
log("Spawning tokens for "..object.getName()..'['..object.getDescription()..']: '..tokenCount.."x "..token)
-- Shift each spawned group after the first down so they don't pile on each other
spawnTokenGroup(object, token, tokenCount, (i - 1) * 0.6)
end
end
else
local data = getPlayerCardData(object)
token = data['tokenType']
tokenCount = data['tokenCount']
log("Spawning tokens for "..object.getName()..'['..object.getDescription()..']: '..tokenCount.."x "..token)
spawnTokenGroup(object, token, tokenCount)
end
markSpawned(object)
end
function resetSpawnState()
local zone = getObjectFromGUID(zoneID)
if zone == nil then return end
for _, object in ipairs(zone.getObjects()) do
if object.tag == "Card" then
unmarkSpawned(object.getGUID(), true)
elseif object.tag == "Deck" then
local cards = object.getObjects()
for _, v in ipairs(cards) do
unmarkSpawned(v.guid)
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
Wait.time(resetSpawnState, 1)
local object = collision_info.collision_object
-- only continue for cards
if object.name ~= "Card" and object.name ~= "CardCustom" then return end
maybeUpdateActiveInvestigator(object)
-- don't spawn tokens for cards in discard pile / threat area
local localpos = self.positionToLocal(object.getPosition())
if localpos.x < -0.7 or localpos.z < -0.3 then
log('Not spawning tokens, relative coordinates are x: ' .. localpos.x .. ' z: ' .. localpos.z)
elseif not object.is_face_down and shouldSpawnTokens(object) then
spawnTokensFor(object)
end
end
---------------------------------------------------------
-- investigator ID grabbing and stat tracker
---------------------------------------------------------
function maybeUpdateActiveInvestigator(card)
local notes = JSON.decode(card.getGMNotes())
if notes ~= nil and notes.type == "Investigator" and notes.id ~= activeInvestigatorId then
activeInvestigatorId = notes.id
STAT_TRACKER.call("updateStats", {notes.willpowerIcons, notes.intellectIcons, notes.combatIcons, notes.agilityIcons})
-- change state of action tokens
local search = searchArea(self.positionToWorld({-1.1, 0.05, -0.27}), {4, 1, 1})
local small_token = nil
local state_table = {
["Guardian"] = 1,
["Seeker"] = 2,
["Rogue"] = 3,
["Mystic"] = 4,
["Survivor"] = 5,
["Neutral"] = 6
}
for _, obj in ipairs(search) do
local obj = obj.hit_object
if obj.getDescription() == "Action Token" and obj.getStateId() > 0 then
if obj.getScale().x < 0.4 then
small_token = obj
else
setObjectState(obj, state_table[notes.class])
end
end
end
-- update the small token with special action for certain investigators
-- Ursula Downs: Investigate action
if activeInvestigatorId == "04002" then
setObjectState(small_token, 8)
-- Daisy Walker (only for normal front, not parallel): Tome action
elseif activeInvestigatorId == "01002" or activeInvestigatorId == "01502" or activeInvestigatorId == "01002-pb" then
setObjectState(small_token, 9)
-- Tony Morgan: Engage/Fight action
elseif activeInvestigatorId == "06003" then
setObjectState(small_token, 10)
-- Finn Edwards: Evade action
elseif activeInvestigatorId == "04003" then
setObjectState(small_token, 11)
-- Bob Jenkins: Play Item action
elseif activeInvestigatorId == "08016" then
setObjectState(small_token, 14)
else
setObjectState(small_token, state_table[notes.class])
end
end
end
function setObjectState(obj, stateId)
if obj.getStateId() ~= stateId then obj.setState(stateId) end
end
---------------------------------------------------------
-- calls to 'Global' / functions for calls from outside
---------------------------------------------------------
function drawChaostokenButton(_, _, isRightClick)
Global.call("drawChaostoken", {self, DRAWN_CHAOS_TOKEN_OFFSET, isRightClick})
end
function drawEncountercard(object, player, isRightClick)
Global.call("drawEncountercard", PLAYER_COLOR)
end
function spawnToken(position, tokenType)
Global.call('spawnToken', {position, tokenType, PLAY_ZONE_ROTATION})
end
-- called by custom data helpers to add player card data
---@param args table Contains only one entry, the GUID of the custom data helper
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
end)
return __bundle_require("__root")