Merge branch 'main' into optionpanel

This commit is contained in:
Chr1Z 2023-01-03 21:57:02 +01:00 committed by GitHub
commit 9dff6886ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
94 changed files with 1764 additions and 875 deletions

View File

@ -57,8 +57,8 @@
"Cluetokens.11e0cf",
"Doomtokens.b015d8",
"DoomCounter.85c4c6",
"Custom_Tile.2eca7c",
"Custom_Tile.fb09d4",
"RoundSequenceActionDescription.2eca7c",
"RoundSequenceActionDescription.fb09d4",
"3DText.65eb7e",
"InvestigatorCount.f182ee",
"ScriptingTrigger.c506bf",
@ -96,7 +96,6 @@
"Resourcetokens.0168ae",
"Horrortokens.ae1a4e",
"DamageTokens.b0ef6c",
"Connectionmarkers.b118af",
"Trash.4b8594",
"Trash.5f896a",
"Trash.147e80",
@ -109,7 +108,6 @@
"Doomtokens.16fcd6",
"DamageTokens.93f4a0",
"Resourcetokens.fd617a",
"Connectionmarkers.5dcccb",
"Cluetokens.31fa39",
"Horrortokens.c3ecf4",
"Doomtokens.47ffc3",
@ -252,7 +250,8 @@
"TokenSpawnTracker.e3ffc9",
"TokenSource.124381",
"GameData.3dbe47",
"SCEDTour.0e5aa8"
"SCEDTour.0e5aa8",
"PlayerCards.2d30ee"
],
"PlayArea": 1,
"PlayerCounts": [

View File

@ -107,12 +107,12 @@
{
"Name": "NextArrow",
"Type": 0,
"URL": "https://i.imgur.com/gXRiKmu.png"
"URL": "https://i.imgur.com/MztSQis.png"
},
{
"Name": "Exit",
"Type": 0,
"URL": "https://i.imgur.com/i0VMjPD.png"
"URL": "https://i.imgur.com/8qmTXwt.png"
},
{
"Name": "Inv-Mandy",

View File

@ -3817,54 +3817,6 @@
"z": 65.419
}
},
{
"Position": {
"x": -22.602,
"y": 1.635,
"z": 22.44
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": -25.571,
"y": 1.639,
"z": 22.44
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": -28.485,
"y": 1.642,
"z": 22.44
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": -31.454,
"y": 1.645,
"z": 22.44
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": -50.852,
@ -3985,106 +3937,6 @@
"z": 0
}
},
{
"Position": {
"x": -28.509,
"y": 1.64,
"z": -22.42
},
"Rotation": {
"x": 0,
"y": 180,
"z": 0
}
},
{
"Position": {
"x": -25.509,
"y": 1.64,
"z": -22.42
},
"Rotation": {
"x": 0,
"y": 180,
"z": 0
}
},
{
"Position": {
"x": -22.606,
"y": 1.64,
"z": -22.42
},
"Rotation": {
"x": 0,
"y": 180,
"z": 0
}
},
{
"Position": {
"x": -19.651,
"y": 1.64,
"z": -22.42
},
"Rotation": {
"x": 0,
"y": 180,
"z": 0
}
},
{
"Position": {
"x": -34.383,
"y": 1.651,
"z": 22.439
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": -16.691,
"y": 1.64,
"z": -22.417
},
"Rotation": {
"x": 0,
"y": 180,
"z": 0
}
},
{
"Position": {
"x": -34.26,
"y": 1.651,
"z": 23.6
}
},
{
"Position": {
"x": -33.12,
"y": 1.65,
"z": 23.6
}
},
{
"Position": {
"x": -16.78,
"y": 1.64,
"z": -23.58
}
},
{
"Position": {
"x": -17.92,
"y": 1.64,
"z": -23.58
}
},
{
"Position": {
"x": -52.07,
@ -4127,20 +3979,6 @@
"z": -21.55
}
},
{
"Position": {
"x": -20.049,
"y": 1.64,
"z": -24.78
}
},
{
"Position": {
"x": -30.997,
"y": 1.647,
"z": 24.81
}
},
{
"Position": {
"x": 45.336,
@ -4203,5 +4041,285 @@
"y": 1.481,
"z": 0
}
},
{
"Position": {
"x": -24.91,
"y": 1.55,
"z": -24.84
}
},
{
"Position": {
"x": -35.77,
"y": 1.55,
"z": 24.86
}
},
{
"Position": {
"x": -37.9,
"y": 1.55,
"z": 23.72
}
},
{
"Position": {
"x": -39.04,
"y": 1.55,
"z": 23.72
}
},
{
"Position": {
"x": -22.78,
"y": 1.55,
"z": -23.72
}
},
{
"Position": {
"x": -21.64,
"y": 1.55,
"z": -23.72
}
},
{
"Position": {
"x": -21.502,
"y": 1.55,
"z": -22.44
},
"Rotation": {
"x": 0,
"y": 180,
"z": 0
}
},
{
"Position": {
"x": -24.462,
"y": 1.55,
"z": -22.44
},
"Rotation": {
"x": 0,
"y": 180,
"z": 0
}
},
{
"Position": {
"x": -27.407,
"y": 1.55,
"z": -22.44
},
"Rotation": {
"x": 0,
"y": 180,
"z": 0
}
},
{
"Position": {
"x": -30.365,
"y": 1.55,
"z": -22.44
},
"Rotation": {
"x": 0,
"y": 180,
"z": 0
}
},
{
"Position": {
"x": -33.332,
"y": 1.55,
"z": -22.44
},
"Rotation": {
"x": 0,
"y": 180,
"z": 0
}
},
{
"Position": {
"x": -36.216,
"y": 1.55,
"z": -22.44
},
"Rotation": {
"x": 0,
"y": 180,
"z": 0
}
},
{
"Position": {
"x": -39.14,
"y": 1.55,
"z": -22.44
},
"Rotation": {
"x": 0,
"y": 180,
"z": 0
}
},
{
"Position": {
"x": -39.271,
"y": 1.55,
"z": 22.44
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": -36.213,
"y": 1.55,
"z": 22.439
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": -33.287,
"y": 1.55,
"z": 22.44
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": -30.332,
"y": 1.55,
"z": 22.44
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": -27.388,
"y": 1.55,
"z": 22.44
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": -24.473,
"y": 1.55,
"z": 22.504
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": -21.541,
"y": 1.55,
"z": 22.504
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": -45.4,
"y": 1.481,
"z": 21.5
}
},
{
"Position": {
"x": -45.4,
"y": 1.481,
"z": 23.75
}
},
{
"Position": {
"x": -45.4,
"y": 1.481,
"z": 26
}
},
{
"Position": {
"x": -45.4,
"y": 1.481,
"z": 28.25
}
},
{
"Position": {
"x": -45.4,
"y": 1.481,
"z": 30.5
}
},
{
"Position": {
"x": -45.4,
"y": 1.481,
"z": -21.5
}
},
{
"Position": {
"x": -45.4,
"y": 1.481,
"z": -23.75
}
},
{
"Position": {
"x": -45.4,
"y": 1.481,
"z": -26
}
},
{
"Position": {
"x": -45.4,
"y": 1.481,
"z": -28.25
}
},
{
"Position": {
"x": -45.4,
"y": 1.481,
"z": -30.5
}
}
]

View File

@ -33,8 +33,8 @@
"IgnoreFoW": false,
"LayoutGroupSortIndex": 0,
"Locked": false,
"LuaScript": "",
"LuaScriptState": "",
"LuaScript_path": "AllPlayerCards.15bb07/ShortSupply.e5f541.ttslua",
"MeasureMovement": false,
"Name": "Card",
"Nickname": "Short Supply",

View File

@ -0,0 +1,36 @@
local playmatAPI = require("playermat/PlaymatApi")
function onLoad()
self.addContextMenuItem("Discard 10 cards", shortSupply)
end
-- called by context menu entry
function shortSupply(color)
local matColor = playmatAPI.getMatColorByPosition(self.getPosition())
-- get draw deck and discard position
local drawDeck = playmatAPI.getDrawDeck(matColor)
local discardPos = playmatAPI.getDiscardPosition(matColor)
-- error handling
if discardPos == nil then
broadcastToColor("Couldn't retrieve discard position from playermat!", color, "Red")
return
end
if drawDeck == nil then
broadcastToColor("Deck not found!", color, "Yellow")
return
elseif drawDeck.tag ~= "Deck" then
broadcastToColor("Deck only contains a single card!", color, "Yellow")
return
end
-- discard cards
broadcastToColor("Discarding top 10 cards for player color '" .. matColor .. "'.", color, "White")
discardPos.y = 0.5
for i = 1, 10 do
discardPos.y = discardPos.y + 0.05 * i
drawDeck.takeObject({ flip = true, position = discardPos })
end
end

View File

@ -51,9 +51,9 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -32.193,
"posX": -36.867,
"posY": 1.52,
"posZ": 30.977,
"posZ": 31.025,
"rotX": 0,
"rotY": 10,
"rotZ": 0,

View File

@ -51,9 +51,9 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -59.426,
"posX": -59.402,
"posY": 1.52,
"posZ": -22.721,
"posZ": -22.586,
"rotX": 0,
"rotY": 280,
"rotZ": 0,

View File

@ -51,9 +51,9 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -18.87,
"posX": -23.89,
"posY": 1.52,
"posZ": -30.977,
"posZ": -31.107,
"rotX": 0,
"rotY": 190,
"rotZ": 0,

View File

@ -51,9 +51,9 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -59.426,
"posX": -59.45,
"posY": 1.52,
"posZ": 9.395,
"posZ": 9.589,
"rotX": 0,
"rotY": 280,
"rotZ": 0,

View File

@ -45,7 +45,7 @@
],
"Tooltip": false,
"Transform": {
"posX": -18.87,
"posX": -23.89,
"posY": 1.3,
"posZ": -30.977,
"rotX": 0,

View File

@ -45,7 +45,7 @@
],
"Tooltip": false,
"Transform": {
"posX": -32.193,
"posX": -36.87,
"posY": 1.3,
"posZ": 30.977,
"rotX": 0,

View File

@ -46,9 +46,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -42.227,
"posY": 1.611,
"posZ": -31.182,
"posX": -45.4,
"posY": 1.561,
"posZ": -28.25,
"rotX": 0,
"rotY": 180,
"rotZ": 0,

View File

@ -46,9 +46,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -44.101,
"posY": 1.632,
"posZ": 31.207,
"posX": -45.4,
"posY": 1.561,
"posZ": 28.25,
"rotX": 0,
"rotY": 0,
"rotZ": 0,

View File

@ -1,61 +0,0 @@
{
"AltLookAngle": {
"x": 0,
"y": 0,
"z": 0
},
"Autoraise": true,
"ColorDiffuse": {
"b": 1,
"g": 1,
"r": 1
},
"ContainedObjects_order": [
"Custom_Tile.7234af"
],
"ContainedObjects_path": "Connectionmarkers.5dcccb",
"CustomMesh": {
"CastShadows": true,
"ColliderURL": "",
"Convex": true,
"DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/949588657208009702/1786DA3A72B61BF39ADE9577B177797450011602/",
"MaterialIndex": 3,
"MeshURL": "https://pastebin.com/raw/ALrYhQGb",
"NormalURL": "",
"TypeIndex": 7
},
"Description": "",
"DragSelectable": true,
"GMNotes": "",
"GUID": "5dcccb",
"Grid": true,
"GridProjection": false,
"Hands": false,
"HideWhenFaceDown": false,
"IgnoreFoW": false,
"LayoutGroupSortIndex": 0,
"Locked": true,
"LuaScript": "",
"LuaScriptState": "",
"MaterialIndex": -1,
"MeasureMovement": false,
"MeshIndex": -1,
"Name": "Custom_Model_Infinite_Bag",
"Nickname": "Connection markers",
"Snap": true,
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -44.559,
"posY": 1.636,
"posZ": -26.724,
"rotX": 0,
"rotY": 180,
"rotZ": 0,
"scaleX": 0.8,
"scaleY": 1,
"scaleZ": 0.8
},
"Value": 0,
"XmlUI": ""
}

View File

@ -1,61 +0,0 @@
{
"AltLookAngle": {
"x": 0,
"y": 0,
"z": 0
},
"Autoraise": true,
"ColorDiffuse": {
"b": 1,
"g": 1,
"r": 1
},
"ContainedObjects_order": [
"Custom_Tile.7234af"
],
"ContainedObjects_path": "Connectionmarkers.b118af",
"CustomMesh": {
"CastShadows": true,
"ColliderURL": "",
"Convex": true,
"DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/949588657208009702/1786DA3A72B61BF39ADE9577B177797450011602/",
"MaterialIndex": 3,
"MeshURL": "https://pastebin.com/raw/ALrYhQGb",
"NormalURL": "",
"TypeIndex": 7
},
"Description": "",
"DragSelectable": true,
"GMNotes": "",
"GUID": "b118af",
"Grid": true,
"GridProjection": false,
"Hands": false,
"HideWhenFaceDown": false,
"IgnoreFoW": false,
"LayoutGroupSortIndex": 0,
"Locked": true,
"LuaScript": "",
"LuaScriptState": "",
"MaterialIndex": -1,
"MeasureMovement": false,
"MeshIndex": -1,
"Name": "Custom_Model_Infinite_Bag",
"Nickname": "Connection markers",
"Snap": true,
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -41.769,
"posY": 1.648,
"posZ": 26.75,
"rotX": 0,
"rotY": 0,
"rotZ": 0,
"scaleX": 0.8,
"scaleY": 1,
"scaleZ": 0.8
},
"Value": 0,
"XmlUI": ""
}

View File

@ -36,9 +36,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -60.574,
"posY": 1.249,
"posZ": 70.866,
"posX": -61,
"posY": 1.27,
"posZ": 74,
"rotX": 0,
"rotY": 270,
"rotZ": 0,

View File

@ -42,11 +42,11 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -32.31,
"posX": -37.182,
"posY": 1.52,
"posZ": 29.006,
"posZ": 29.089,
"rotX": 0,
"rotY": 11,
"rotY": 10,
"rotZ": 1,
"scaleX": 0.26,
"scaleY": 1,

View File

@ -42,11 +42,11 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -18.706,
"posX": -23.497,
"posY": 1.52,
"posZ": -29.027,
"posZ": -29.078,
"rotX": 0,
"rotY": 195,
"rotY": 190,
"rotZ": 0,
"scaleX": 0.26,
"scaleY": 1,

View File

@ -42,11 +42,11 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -57.512,
"posX": -57.507,
"posY": 1.52,
"posZ": -22.921,
"posZ": -22.894,
"rotX": 0,
"rotY": 285,
"rotY": 280,
"rotZ": 0,
"scaleX": 0.26,
"scaleY": 1,

View File

@ -42,11 +42,11 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -57.488,
"posX": -57.472,
"posY": 1.52,
"posZ": 9.184,
"posZ": 9.273,
"rotX": 0,
"rotY": 282,
"rotY": 280,
"rotZ": 0,
"scaleX": 0.26,
"scaleY": 1,

View File

@ -56,9 +56,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -44.558,
"posY": 1.635,
"posZ": -28.959,
"posX": -45.4,
"posY": 1.581,
"posZ": -23.75,
"rotX": 0,
"rotY": 180,
"rotZ": 0,

View File

@ -56,9 +56,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -41.77,
"posY": 1.648,
"posZ": 28.985,
"posX": -45.4,
"posY": 1.581,
"posZ": 23.75,
"rotX": 0,
"rotY": 0,
"rotZ": 0,

View File

@ -43,9 +43,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -60.384,
"posY": 1.325,
"posZ": 86.713,
"posX": -61,
"posY": 1.27,
"posZ": 89,
"rotX": 0,
"rotY": 270,
"rotZ": 0,

View File

@ -46,9 +46,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -41.77,
"posY": 1.649,
"posZ": 31.207,
"posX": -45.4,
"posY": 1.581,
"posZ": 30.5,
"rotX": 0,
"rotY": 0,
"rotZ": 0,

View File

@ -46,9 +46,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -44.558,
"posY": 1.634,
"posZ": -31.182,
"posX": -45.4,
"posY": 1.581,
"posZ": -30.5,
"rotX": 0,
"rotY": 180,
"rotZ": 0,

View File

@ -17,7 +17,7 @@
"ChaosBagManager.023240",
"TokenArranger.022907",
"CYOACampaignGuides.e87ea2",
"AttachmentHelper.d45664",
"AttachmentHelper.7f4976",
"ArkhamFantasy-PixelArtMini-Cards.e17c9e",
"jaqenZannsNavigationOverlay.a8affa",
"DrawTokenButtonTooltipRenamer.cc77a8",

View File

@ -0,0 +1,51 @@
{
"AltLookAngle": {
"x": 0,
"y": 0,
"z": 0
},
"Autoraise": true,
"ColorDiffuse": {
"b": 1,
"g": 0.37256,
"r": 0.30589
},
"ContainedObjects_order": [
"AttachmentHelper.d45664"
],
"ContainedObjects_path": "AttachmentHelper.7f4976",
"Description": "Provides card-sized bags that are useful for cards that are attached facedown (e.g. Backpack).",
"DragSelectable": true,
"GMNotes": "",
"GUID": "7f4976",
"Grid": true,
"GridProjection": false,
"Hands": false,
"HideWhenFaceDown": false,
"IgnoreFoW": false,
"LayoutGroupSortIndex": 0,
"Locked": false,
"LuaScript": "",
"LuaScriptState": "",
"MaterialIndex": -1,
"MeasureMovement": false,
"MeshIndex": -1,
"Name": "Infinite_Bag",
"Nickname": "Attachment Helper",
"Snap": true,
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": 27.677,
"posY": 4.472,
"posZ": -31.034,
"rotX": 0,
"rotY": 0,
"rotZ": 0,
"scaleX": 1,
"scaleY": 1,
"scaleZ": 1
},
"Value": 0,
"XmlUI": ""
}

View File

@ -0,0 +1,65 @@
{
"AltLookAngle": {
"x": 0,
"y": 0,
"z": 0
},
"Autoraise": true,
"Bag": {
"Order": 0
},
"ColorDiffuse": {
"b": 1,
"g": 1,
"r": 1
},
"CustomMesh": {
"CastShadows": true,
"ColliderURL": "http://cloud-3.steamusercontent.com/ugc/1754695414379239413/0B8E68F3B7311DCF2138FB701F78D1D93FBA4CAB/",
"Convex": true,
"DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/1750192233783143973/D526236AAE16BDBB98D3F30E27BAFC1D3E21F4AC/",
"MaterialIndex": 1,
"MeshURL": "http://cloud-3.steamusercontent.com/ugc/1754695414379239413/0B8E68F3B7311DCF2138FB701F78D1D93FBA4CAB/",
"NormalURL": "",
"TypeIndex": 6
},
"Description": "Drop cards here to display name, cost and skill icons.\n\nSee context menu for options.",
"DragSelectable": true,
"GMNotes": "",
"GUID": "d45664",
"Grid": true,
"GridProjection": false,
"Hands": false,
"HideWhenFaceDown": false,
"IgnoreFoW": false,
"LayoutGroupSortIndex": 0,
"Locked": false,
"LuaScriptState": "[[],true,true]",
"LuaScript_path": "Fan-MadeAccessories.aa8b38/AttachmentHelper.7f4976/AttachmentHelper.d45664.ttslua",
"MaterialIndex": -1,
"MeasureMovement": false,
"MeshIndex": -1,
"Name": "Custom_Model_Bag",
"Nickname": "Attachment Helper",
"Number": 0,
"Snap": true,
"Sticky": true,
"Tags": [
"Asset",
"scesetup_memory_object"
],
"Tooltip": true,
"Transform": {
"posX": 19.228,
"posY": 3.822,
"posZ": -19.636,
"rotX": 0,
"rotY": 270,
"rotZ": 359,
"scaleX": 0.8,
"scaleY": 1,
"scaleZ": 0.8
},
"Value": 0,
"XmlUI": ""
}

View File

@ -0,0 +1,202 @@
local OPTION_TEXT = {
"Ancestral Knowledge",
"Astronomical Atlas",
"Crystallizer of Dreams",
"Diana Stanley",
"Gloria Goldberg",
"Sefina Rousseau",
"Wooden Sledge"
}
local IMAGE_LIST = {
-- Ancestral Knowledge
"http://cloud-3.steamusercontent.com/ugc/1915746489207287888/2F9F6F211ED0F98E66C9D35D93221E4C7FB6DD3C/",
-- Astronomical Atlas
"http://cloud-3.steamusercontent.com/ugc/1754695853007989004/9153BC204FC707AE564ECFAC063A11CB8C2B5D1E/",
-- Crystallizer of Dreams
"http://cloud-3.steamusercontent.com/ugc/1915746489207280958/100F16441939E5E23818651D1EB5C209BF3125B9/",
-- Diana Stanley
"http://cloud-3.steamusercontent.com/ugc/1754695635919071208/1AB7222850201630826BFFBA8F2BD0065E2D572F/",
-- Gloria Goldberg
"http://cloud-3.steamusercontent.com/ugc/1754695635919102502/453D4426118C8A6DE2EA281184716E26CA924C84/",
-- Sefina Rousseau
"http://cloud-3.steamusercontent.com/ugc/1754695635919099826/3C3CBFFAADB2ACA9957C736491F470AE906CC953/",
-- Wooden Sledge
"http://cloud-3.steamusercontent.com/ugc/1750192233783143973/D526236AAE16BDBB98D3F30E27BAFC1D3E21F4AC/"
}
-- save state and options to restore onLoad
function onSave() return JSON.encode({ cardsInBag, showCost, showIcons }) end
-- load variables and create context menu
function onLoad(savedData)
local loadedData = JSON.decode(savedData)
cardsInBag = loadedData[1] or {}
showCost = loadedData[2] or true
showIcons = loadedData[3] or true
recreateButtons()
self.addContextMenuItem("Select image", selectImage)
self.addContextMenuItem("Toggle cost", function(color)
showCost = not showCost
printToColor("Show cost of cards: " .. tostring(showCost), color, "White")
refresh()
end)
self.addContextMenuItem("Toggle skill icons", function(color)
showIcons = not showIcons
printToColor("Show skill icons of cards: " .. tostring(showIcons), color, "White")
refresh()
end)
self.addContextMenuItem("More Information", function()
printToAll("------------------------------", "White")
printToAll("Attachment Helper by Chr1Z", "Orange")
printToAll("original by bankey", "White")
end)
end
function selectImage(color)
Player[color].showOptionsDialog("Select image:", OPTION_TEXT, 1, function(_, option_index)
local customInfo = self.getCustomObject()
customInfo.diffuse = IMAGE_LIST[option_index]
self.setCustomObject(customInfo)
self.reload()
end)
end
-- called for every card that enters
function onObjectEnterContainer(container, object)
if container == self then
if object.tag ~= "Card" then
broadcastToAll("The 'Attachment Helper' is meant to be used for single cards.", "White")
else
findCard(object.getGUID(), object.getName(), object.getGMNotes())
end
-- TODO: implement splitting of decks that get thrown in here
recreateButtons()
end
end
-- removes leaving cards from the "cardInBag" table
function onObjectLeaveContainer(container, object)
if container == self then
local guid = object.getGUID()
local found = false
for i, card in ipairs(cardsInBag) do
if card.id == guid then
table.remove(cardsInBag, i)
found = true
break
end
end
if found ~= true then
local name = object.getName()
for i, card in ipairs(cardsInBag) do
if card.name == name then
table.remove(cardsInBag, i)
break
end
end
end
recreateButtons()
end
end
-- refreshes displayed buttons based on contained cards
function refresh()
cardsInBag = {}
for _, object in ipairs(self.getObjects()) do
findCard(object.guid, object.name, object.gm_notes)
end
recreateButtons()
end
-- gets cost and icons for a card
function findCard(guid, name, GMNotes)
local cost = ""
local icons = {}
local metadata = {}
local displayName = name
if displayName == nil or displayName == "" then displayName = "unnamed" end
if showCost or showIcons then metadata = JSON.decode(GMNotes) end
if showCost then
if GMNotes ~= "" then cost = metadata.cost end
if cost == nil or cost == "" then cost = "" end
displayName = "[" .. cost .. "] " .. displayName
end
if showIcons then
if GMNotes ~= "" then
icons[1] = metadata.wildIcons
icons[2] = metadata.willpowerIcons
icons[3] = metadata.intellectIcons
icons[4] = metadata.combatIcons
icons[5] = metadata.agilityIcons
end
local IconTypes = { "Wild", "Willpower", "Intellect", "Combat", "Agility" }
local found = false
for i = 1, 5 do
if icons[i] ~= nil and icons[i] ~= "" then
if found == false then
displayName = displayName .. "\n" .. IconTypes[i] .. ": " .. icons[i]
found = true
else
displayName = displayName .. " " .. IconTypes[i] .. ": " .. icons[i]
end
end
end
end
table.insert(cardsInBag, { name = name, displayName = displayName, id = guid })
end
-- recreates buttons with up-to-date labels
function recreateButtons()
self.clearButtons()
local verticalPosition = 1.65
for _, card in ipairs(cardsInBag) do
local id = card.id
local funcName = "removeCard" .. id
self.setVar(funcName, function() removeCard(id) end)
self.createButton({
label = card.displayName,
click_function = funcName,
function_owner = self,
position = { 0, 0, verticalPosition },
height = 200,
width = 1200,
font_size = string.len(card.displayName) > 20 and 75 or 100
})
verticalPosition = verticalPosition - 0.5
end
local countLabel = "Attachment\nHelper"
if #cardsInBag ~= 0 then countLabel = #cardsInBag end
self.createButton({
label = countLabel,
click_function = "none",
function_owner = self,
position = { 0, 0, -1.35 },
height = 0,
width = 0,
font_size = 225,
font_color = { 1, 1, 1 }
})
end
-- click-function for buttons to take a card out of the bag
function removeCard(cardGUID)
self.takeObject({
guid = cardGUID,
rotation = self.getRotation(),
position = self.getPosition() + Vector(0, 0.25, 0),
callback_function = function(obj) obj.resting = true end
})
end

View File

@ -41,6 +41,7 @@
"Snap": true,
"Sticky": true,
"Tags": [
"CleanUpHelper_ignore",
"displacement_excluded"
],
"Tooltip": true,

View File

@ -40,6 +40,10 @@
"Nickname": "Hand Helper",
"Snap": true,
"Sticky": true,
"Tags": [
"CleanUpHelper_ignore",
"displacement_excluded"
],
"Tooltip": true,
"Transform": {
"posX": 37.613,

View File

@ -1,7 +1,7 @@
MAT_GUIDS = { "8b081b", "bd0ff4", "383d8b", "0840d5" }
local playmatAPI = require("playermat/PlaymatApi")
local BUTTON_PARAMETERS = {}
BUTTON_PARAMETERS.function_owner = self
local buttonParamaters = {}
buttonParamaters.function_owner = self
-- saving "playerColor" and "des"
function onSave() return JSON.encode({ playerColor, des}) end
@ -13,41 +13,41 @@ function onLoad(saved_data)
des = loaded_data[2] or false
-- index 0: button as hand size label
BUTTON_PARAMETERS.hover_color = "White"
BUTTON_PARAMETERS.click_function = "none"
BUTTON_PARAMETERS.position = { 0, 0.1, -0.4 }
BUTTON_PARAMETERS.height = 0
BUTTON_PARAMETERS.width = 0
BUTTON_PARAMETERS.font_size = 500
BUTTON_PARAMETERS.font_color = "White"
self.createButton(BUTTON_PARAMETERS)
buttonParamaters.hover_color = "White"
buttonParamaters.click_function = "none"
buttonParamaters.position = { 0, 0.11, -0.4 }
buttonParamaters.height = 0
buttonParamaters.width = 0
buttonParamaters.font_size = 500
buttonParamaters.font_color = "White"
self.createButton(buttonParamaters)
-- index 1: button to toggle "des"
BUTTON_PARAMETERS.label = "DES: " .. (des and "✓" or "✗")
BUTTON_PARAMETERS.click_function = "toggleDES"
BUTTON_PARAMETERS.position = { 0.475, 0.1, 0.25 }
BUTTON_PARAMETERS.height = 175
BUTTON_PARAMETERS.width = 440
BUTTON_PARAMETERS.font_size = 90
BUTTON_PARAMETERS.font_color = "Black"
self.createButton(BUTTON_PARAMETERS)
buttonParamaters.label = "DES: " .. (des and "✓" or "✗")
buttonParamaters.click_function = "toggleDES"
buttonParamaters.position = { 0.475, 0.11, 0.25 }
buttonParamaters.height = 175
buttonParamaters.width = 440
buttonParamaters.font_size = 90
buttonParamaters.font_color = "Black"
self.createButton(buttonParamaters)
-- index 2: button to discard a card
BUTTON_PARAMETERS.label = "discard random card"
BUTTON_PARAMETERS.click_function = "discardRandom"
BUTTON_PARAMETERS.position = { 0, 0.1, 0.7 }
BUTTON_PARAMETERS.width = 900
self.createButton(BUTTON_PARAMETERS)
buttonParamaters.label = "discard random card"
buttonParamaters.click_function = "discardRandom"
buttonParamaters.position = { 0, 0.11, 0.7 }
buttonParamaters.width = 900
self.createButton(buttonParamaters)
-- index 3: button to select color
BUTTON_PARAMETERS.label = playerColor
BUTTON_PARAMETERS.color = playerColor
BUTTON_PARAMETERS.hover_color = playerColor
BUTTON_PARAMETERS.click_function = "changeColor"
BUTTON_PARAMETERS.tooltip = "change color"
BUTTON_PARAMETERS.position = { -0.475, 0.1, 0.25 }
BUTTON_PARAMETERS.width = 440
self.createButton(BUTTON_PARAMETERS)
buttonParamaters.label = playerColor
buttonParamaters.color = playerColor
buttonParamaters.hover_color = playerColor
buttonParamaters.click_function = "changeColor"
buttonParamaters.tooltip = "change color"
buttonParamaters.position = { -0.475, 0.11, 0.25 }
buttonParamaters.width = 440
self.createButton(buttonParamaters)
-- start loop to update card count
loopId = Wait.time(||updateValue(), 1, -1)
@ -71,14 +71,6 @@ function onLoad(saved_data)
end
function onObjectHover(hover_color, obj)
-- error handling
if obj == nil then return end
-- add context menu to "short supply"
if obj.getName() == "Short Supply" then
obj.addContextMenuItem("Discard 10 (" .. playerColor .. ")", shortSupply)
end
-- only continue if correct player hovers over "self"
if obj ~= self or hover_color ~= playerColor then return end
@ -151,11 +143,11 @@ function changeColor(_, _, isRightClick, color)
end
-- update "change color" button (note: remove and create instantly updates hover_color)
BUTTON_PARAMETERS.label = playerColor
BUTTON_PARAMETERS.color = playerColor
BUTTON_PARAMETERS.hover_color = playerColor
buttonParamaters.label = playerColor
buttonParamaters.color = playerColor
buttonParamaters.hover_color = playerColor
self.removeButton(3)
self.createButton(BUTTON_PARAMETERS)
self.createButton(buttonParamaters)
end
---------------------------------------------------------
@ -169,7 +161,8 @@ function discardRandom()
if #hand == 0 then
broadcastToAll("Cannot discard from empty hand!", "Red")
else
local mat = getPlayermat(playerColor)
local searchPos = Player[playerColor].getHandTransform().position
local mat = playmatAPI.getMatbyPosition(searchPos)
if mat == nil then return end
local discardPos = mat.getTable("DISCARD_PILE_POSITION")
@ -184,38 +177,6 @@ function discardRandom()
end
end
---------------------------------------------------------
-- discards the top 10 cards of your deck
---------------------------------------------------------
function shortSupply(color)
local mat = getPlayermat(playerColor)
if mat == nil then return end
-- get draw deck and discard pile
mat.call("getDrawDiscardDecks")
drawDeck = mat.getVar("drawDeck")
local discardPos = mat.getTable("DISCARD_PILE_POSITION")
if discardPos == nil then
broadcastToAll("Couldn't retrieve discard position from playermat!", "Red")
return
end
if drawDeck == nil then
broadcastToColor("Deck not found!", color, "Yellow")
return
elseif drawDeck.tag ~= "Deck" then
broadcastToColor("Deck only contains a single card!", color, "Yellow")
return
end
-- discard cards
discardPos[2] = 0.5
for i = 1, 10 do
discardPos[2] = discardPos[2] + 0.05 * i
drawDeck.takeObject({ flip = true; position = discardPos })
end
end
---------------------------------------------------------
-- helper functions
---------------------------------------------------------
@ -232,27 +193,3 @@ function playerExists(color)
local COLORS = Player.getAvailableColors()
return indexOf(COLORS, color) and true or false
end
-- helper to find playermat based on hand position
function getPlayermat(color)
local pos = Player[playerColor].getHandTransform().position
if pos.x < -30 then
if pos.z > 0 then
playerNumber = 1
else
playerNumber = 2
end
else
if pos.z > 0 then
playerNumber = 3
else
playerNumber = 4
end
end
local mat = getObjectFromGUID(MAT_GUIDS[playerNumber])
if mat == nil then
broadcastToAll(playerColor .. " playermat could not be found!", "Yellow")
end
return mat
end

View File

@ -32,7 +32,7 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -27.96,
"posX": -30.5,
"posY": 6,
"posZ": 36.053,
"rotX": 0,

View File

@ -32,9 +32,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -65.72,
"posX": -65.7,
"posY": 6,
"posZ": -13.61,
"posZ": -15.5,
"rotX": 0,
"rotY": 90,
"rotZ": 0,

View File

@ -32,9 +32,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -65.581,
"posX": -65.7,
"posY": 6,
"posZ": 13.55,
"posZ": 15.5,
"rotX": 0,
"rotY": 90,
"rotZ": 0,

View File

@ -32,7 +32,7 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -27.96,
"posX": -30.5,
"posY": 6,
"posZ": -36.364,
"rotX": 0,

View File

@ -42,11 +42,11 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -57.882,
"posX": -57.887,
"posY": 1.52,
"posZ": -24.902,
"posZ": -24.928,
"rotX": 0,
"rotY": 285,
"rotY": 280,
"rotZ": 0,
"scaleX": 0.26,
"scaleY": 1,

View File

@ -42,9 +42,9 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -57.837,
"posX": -57.83,
"posY": 1.52,
"posZ": 7.19,
"posZ": 7.229,
"rotX": 0,
"rotY": 280,
"rotZ": 0,

View File

@ -42,9 +42,9 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -34.234,
"posY": 1.52,
"posZ": 29.394,
"posX": -39.163,
"posY": 1.519,
"posZ": 29.487,
"rotX": 0,
"rotY": 10,
"rotZ": 1,

View File

@ -42,11 +42,11 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -16.653,
"posX": -21.469,
"posY": 1.52,
"posZ": -29.429,
"posZ": -29.42,
"rotX": 0,
"rotY": 195,
"rotY": 190,
"rotZ": 0,
"scaleX": 0.26,
"scaleY": 1,

View File

@ -46,9 +46,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -42.227,
"posY": 1.632,
"posZ": -28.959,
"posX": -45.4,
"posY": 1.581,
"posZ": -26,
"rotX": 0,
"rotY": 180,
"rotZ": 0,

View File

@ -46,9 +46,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -44.101,
"posY": 1.652,
"posZ": 28.985,
"posX": -45.4,
"posY": 1.581,
"posZ": 26,
"rotX": 0,
"rotY": 0,
"rotZ": 0,

View File

@ -46,7 +46,7 @@
],
"Tooltip": true,
"Transform": {
"posX": -32.6,
"posX": -37.35,
"posY": 1.531,
"posZ": 19.35,
"rotX": 0,

View File

@ -46,7 +46,7 @@
],
"Tooltip": true,
"Transform": {
"posX": -47.76,
"posX": -47.75,
"posY": 1.531,
"posZ": -23.1,
"rotX": 0,

View File

@ -46,7 +46,7 @@
],
"Tooltip": true,
"Transform": {
"posX": -18.6,
"posX": -23.35,
"posY": 1.531,
"posZ": -19.35,
"rotX": 0,

View File

@ -1157,7 +1157,7 @@
],
"Tooltip": true,
"Transform": {
"posX": -19.17,
"posX": -23.92,
"posY": 1.55,
"posZ": -24.845,
"rotX": 0,

View File

@ -1157,7 +1157,7 @@
],
"Tooltip": true,
"Transform": {
"posX": -34.293,
"posX": -39.043,
"posY": 1.55,
"posZ": 24.864,
"rotX": 0,

View File

@ -1157,7 +1157,7 @@
],
"Tooltip": true,
"Transform": {
"posX": -18.025,
"posX": -22.776,
"posY": 1.55,
"posZ": -24.845,
"rotX": 0,

View File

@ -1157,9 +1157,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -20.049,
"posX": -24.91,
"posY": 1.55,
"posZ": -24.78,
"posZ": -24.84,
"rotX": 0,
"rotY": 180,
"rotZ": 0,

View File

@ -1157,7 +1157,7 @@
],
"Tooltip": true,
"Transform": {
"posX": -32.011,
"posX": -36.761,
"posY": 1.55,
"posZ": 24.864,
"rotX": 0,

View File

@ -1157,9 +1157,9 @@
],
"Tooltip": true,
"Transform": {
"posX": -30.997,
"posX": -35.77,
"posY": 1.55,
"posZ": 24.81,
"posZ": 24.86,
"rotX": 0,
"rotY": 0,
"rotZ": 0,

View File

@ -1157,7 +1157,7 @@
],
"Tooltip": true,
"Transform": {
"posX": -16.887,
"posX": -21.638,
"posY": 1.55,
"posZ": -24.845,
"rotX": 0,

View File

@ -1157,7 +1157,7 @@
],
"Tooltip": true,
"Transform": {
"posX": -33.149,
"posX": -37.899,
"posY": 1.55,
"posZ": 24.864,
"rotX": 0,

View File

@ -1 +1 @@
{"optionPanel":{"showAttachmentHelper":false,"showChaosBagManager":false,"showCleanUpHelper":false,"showCustomPlaymatImages":false,"showCYOA":false,"showDisplacementTool":false,"showDrawButton":false,"showHandHelper":[],"showNavigationOverlay":false,"showTokenArranger":false,"useClueClickers":false,"useSnapTags":true}}
{"optionPanel":{"showAttachmentHelper":false,"showChaosBagManager":false,"showCleanUpHelper":false,"useClueClickers":false,"showCustomPlaymatImages":false,"showCYOA":false,"showDisplacementTool":false,"showDrawButton":false,"showHandHelper":[],"showNavigationOverlay":false,"useSnapTags":true,"showTitleSplash":true,"showTokenArranger":false}}

View File

@ -0,0 +1,57 @@
{
"AltLookAngle": {
"x": 0,
"y": 0,
"z": 0
},
"Autoraise": true,
"ColorDiffuse": {
"b": 1,
"g": 1,
"r": 1
},
"CustomImage": {
"CustomTile": {
"Stackable": false,
"Stretch": true,
"Thickness": 0.1,
"Type": 3
},
"ImageScalar": 1,
"ImageSecondaryURL": "https://i.imgur.com/dISlnEk.jpg",
"ImageURL": "https://i.imgur.com/dISlnEk.jpg",
"WidthScale": 0
},
"Description": "",
"DragSelectable": true,
"GMNotes": "",
"GUID": "2d30ee",
"Grid": true,
"GridProjection": false,
"Hands": false,
"HideWhenFaceDown": false,
"IgnoreFoW": false,
"LayoutGroupSortIndex": 0,
"Locked": false,
"LuaScript": "require(\"playercards/PlayerCardPanel\")",
"LuaScriptState": "{\"spawnBagState\":{\"placed\":[],\"placedObjects\":[]}}",
"MeasureMovement": false,
"Name": "Custom_Tile",
"Nickname": "Player Cards",
"Snap": true,
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -1.083,
"posY": 1.255,
"posZ": 69.985,
"rotX": 0,
"rotY": 270,
"rotZ": 0,
"scaleX": 9.39,
"scaleY": 1,
"scaleZ": 9.39
},
"Value": 0,
"XmlUI": ""
}

View File

@ -145,30 +145,6 @@
"Asset"
]
},
{
"Position": {
"x": 1.37,
"y": 0.1,
"z": -0.637
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": 0.914,
"y": 0.1,
"z": -0.637
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": -1.817,
@ -282,7 +258,7 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -25.6,
"posX": -30.35,
"posY": 1.45,
"posZ": 26.6,
"rotX": 0,

View File

@ -179,30 +179,6 @@
"z": 0
}
},
{
"Position": {
"x": 0.911,
"y": 0.1,
"z": -0.641
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": 1.367,
"y": 0.1,
"z": -0.641
},
"Rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
{
"Position": {
"x": -0.982,
@ -282,7 +258,7 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -25.6,
"posX": -30.35,
"posY": 1.45,
"posZ": -26.6,
"rotX": 0,

View File

@ -42,9 +42,9 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -59.774,
"posX": -59.817,
"posY": 1.52,
"posZ": 7.535,
"posZ": 7.617,
"rotX": 0,
"rotY": 280,
"rotZ": 0,

View File

@ -42,11 +42,11 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -59.793,
"posX": -59.798,
"posY": 1.52,
"posZ": -24.544,
"posZ": -24.571,
"rotX": 0,
"rotY": 285,
"rotY": 280,
"rotZ": 0,
"scaleX": 0.26,
"scaleY": 1,

View File

@ -42,11 +42,11 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -17.037,
"posX": -21.914,
"posY": 1.52,
"posZ": -31.384,
"posZ": -31.433,
"rotX": 0,
"rotY": 195,
"rotY": 190,
"rotZ": 0,
"scaleX": 0.26,
"scaleY": 1,

View File

@ -42,9 +42,9 @@
"Sticky": true,
"Tooltip": false,
"Transform": {
"posX": -33.889,
"posX": -38.812,
"posY": 1.52,
"posZ": 31.335,
"posZ": 31.434,
"rotX": 0,
"rotY": 10,
"rotZ": 0,

View File

@ -46,9 +46,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -44.105,
"posY": 1.651,
"posZ": 26.75,
"posX": -45.4,
"posY": 1.581,
"posZ": 21.5,
"rotX": 0,
"rotY": 0,
"rotZ": 0,

View File

@ -46,9 +46,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -42.223,
"posY": 1.633,
"posZ": -26.724,
"posX": -45.4,
"posY": 1.581,
"posZ": -21.5,
"rotX": 0,
"rotY": 180,
"rotZ": 0,

View File

@ -37,14 +37,14 @@
"LuaScriptState": "",
"MeasureMovement": false,
"Name": "Custom_Tile",
"Nickname": "",
"Nickname": "Round Sequence / Action Description",
"Snap": true,
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -43.21,
"posY": 1.55,
"posZ": 22.5,
"posX": -57,
"posY": 1.255,
"posZ": 48,
"rotX": 0,
"rotY": 270,
"rotZ": 0,

View File

@ -37,14 +37,14 @@
"LuaScriptState": "",
"MeasureMovement": false,
"Name": "Custom_Tile",
"Nickname": "",
"Nickname": "Round Sequence / Action Description",
"Snap": true,
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -43.21,
"posY": 1.55,
"posZ": -22.5,
"posX": -65,
"posY": 1.355,
"posZ": 48,
"rotX": 0,
"rotY": 270,
"rotZ": 180,

View File

@ -43,9 +43,9 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -60.748,
"posY": 1.316,
"posZ": 54.855,
"posX": -61,
"posY": 1.27,
"posZ": 59,
"rotX": 0,
"rotY": 270,
"rotZ": 0,

View File

@ -31,7 +31,7 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -14.05,
"posX": -18.8,
"posY": 1.481,
"posZ": -28.6,
"rotX": 0,

View File

@ -31,7 +31,7 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -27.977,
"posX": -30.5,
"posY": 4.076,
"posZ": -37.889,
"rotX": 0,

View File

@ -31,7 +31,7 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -28.046,
"posX": -30.5,
"posY": 4.065,
"posZ": 37.669,
"rotX": 0,

View File

@ -33,7 +33,7 @@
"Transform": {
"posX": -66.804,
"posY": 4.135,
"posZ": 13.565,
"posZ": 15.5,
"rotX": 0,
"rotY": 90,
"rotZ": 0,

View File

@ -33,7 +33,7 @@
"Transform": {
"posX": -66.963,
"posY": 4.117,
"posZ": -13.277,
"posZ": -15.5,
"rotX": 0,
"rotY": 90,
"rotZ": 0,

View File

@ -31,7 +31,7 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -37.15,
"posX": -41.9,
"posY": 1.468,
"posZ": 28.6,
"rotX": 0,

View File

@ -18,7 +18,7 @@
"Damage.cd2a02",
"Horror.36be72",
"ClueDoom.a3fb6c",
"Reesource.00d19a"
"Resource.00d19a"
],
"ContainedObjects_path": "TokenSource.124381",
"Description": "",

View File

@ -40,7 +40,7 @@
"Nickname": "ClueDoom",
"Snap": false,
"Sticky": true,
"Tooltip": false,
"Tooltip": true,
"Transform": {
"posX": 78.661,
"posY": 2.398,

View File

@ -40,7 +40,7 @@
"Nickname": "ClueDoom",
"Snap": false,
"Sticky": true,
"Tooltip": false,
"Tooltip": true,
"Transform": {
"posX": 78.738,
"posY": 2.287,

View File

@ -37,10 +37,10 @@
"LuaScriptState": "",
"MeasureMovement": false,
"Name": "Custom_Token",
"Nickname": "Reesource",
"Nickname": "Resource",
"Snap": false,
"Sticky": true,
"Tooltip": false,
"Tooltip": true,
"Transform": {
"posX": 78.848,
"posY": 2.273,

View File

@ -46,7 +46,7 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -37.497,
"posX": -42.25,
"posY": 1.653,
"posZ": -19.3,
"rotX": 0,

View File

@ -46,7 +46,7 @@
"Sticky": true,
"Tooltip": true,
"Transform": {
"posX": -37.5,
"posX": -42.25,
"posY": 1.664,
"posZ": 19.3,
"rotX": 0,

View File

@ -134,15 +134,6 @@ do
internal.maybePrint(table.concat({ "Found decklist: ", deck.name }), playerColor)
log(table.concat({ "-", deck.name, "-" }))
for k, v in pairs(deck) do
if type(v) == "table" then
log(table.concat { k, ": <table>" })
else
log(table.concat { k, ": ", tostring(v) })
end
end
-- 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

View File

@ -34,6 +34,7 @@ local chaosTokens = {}
local chaosTokensLastMat = nil
local IS_RESHUFFLING = false
local bagSearchers = {}
local hideTitleSplashWaitFunctionId = nil
local playmatAPI = require("playermat/PlaymatApi")
---------------------------------------------------------
@ -818,6 +819,10 @@ function applyOptionPanelChange(id, state)
-- update master clue counter
getObjectFromGUID("4a3aa4").setVar("useClickableCounters", state)
-- option: Show Title on placing scenarios
elseif id == "showTitleSplash" then
optionPanel[id] = state
-- option: Show token arranger
elseif id == "showTokenArranger" then
-- delete previously pulled out tokens
@ -833,8 +838,8 @@ function applyOptionPanelChange(id, state)
elseif id == "showHandHelper" then
optionPanel[id][1] = spawnOrRemoveHelper(state, "Hand Helper", {-50.84, 1.6, 7.02}, {0, 270, 0}, "White")
optionPanel[id][2] = spawnOrRemoveHelper(state, "Hand Helper", {-50.90, 1.6, -25.10}, {0, 270, 0}, "Orange")
optionPanel[id][3] = spawnOrRemoveHelper(state, "Hand Helper", {-34.38, 1.6, 22.44}, {0, 000, 0}, "Green")
optionPanel[id][4] = spawnOrRemoveHelper(state, "Hand Helper", {-16.69, 1.6, -22.42}, {0, 180, 0}, "Red")
optionPanel[id][3] = spawnOrRemoveHelper(state, "Hand Helper", {-39.27, 1.6, 22.44}, {0, 000, 0}, "Green")
optionPanel[id][4] = spawnOrRemoveHelper(state, "Hand Helper", {-21.51, 1.6, -22.44}, {0, 180, 0}, "Red")
-- option: Show chaos bag manager
elseif id == "showChaosBagManager" then
@ -951,7 +956,7 @@ function onClick_defaultSettings()
for id, _ in pairs(optionPanel) do
local state = false
-- override for settings that are enabled by default
if id == "useSnapTags" then
if id == "useSnapTags" or id == "showTitleSplash" then
state = true
end
applyOptionPanelChange(id, state)
@ -962,6 +967,7 @@ function onClick_defaultSettings()
useSnapTags = true,
showDrawButton = false,
useClueClickers = false,
showTitleSplash = true,
showTokenArranger = false,
showCleanUpHelper = false,
showHandHelper = {},
@ -972,3 +978,25 @@ function onClick_defaultSettings()
-- update UI
updateOptionPanelState()
end
-- splash scenario title on setup
function titleSplash(scenarioName)
if optionPanel['showTitleSplash'] then
-- if there's any ongoing title being displayed, hide it and cancel the waiting function
if hideTitleSplashWaitFunctionId then
Wait.stop(hideTitleSplashWaitFunctionId)
hideTitleSplashWaitFunctionId = nil
UI.setAttribute('title_splash', 'active', false)
end
-- display scenario name and set a 4 seconds (2 seconds animation and 2 seconds on screen)
-- wait timer to hide the scenario name
UI.setValue('title_splash', scenarioName)
UI.show('title_splash')
hideTitleSplashWaitFunctionId = Wait.time(function()
UI.hide('title_splash')
hideTitleSplashWaitFunctionId = nil
end, 4)
end
end

View File

@ -55,6 +55,7 @@ function resetTokensIfInDeckZone(container, object)
end
function fireScenarioChangedEvent()
Global.call('titleSplash', currentScenario)
playArea.onScenarioChanged(currentScenario)
end

View File

@ -5,10 +5,24 @@ local tokenManager = require("core/token/TokenManager")
---------------------------------------------------------
-- set true to enable debug logging
DEBUG = false
local DEBUG = false
-- Location connection directional options
local BIDIRECTIONAL = 0
local ONE_WAY = 1
-- Connector draw parameters
local CONNECTION_THICKNESS = 0.015
local CONNECTION_COLOR = { 0.4, 0.4, 0.4, 1 }
local DIRECTIONAL_ARROW_DISTANCE = 3.5
local ARROW_ARM_LENGTH = 0.9
local ARROW_ANGLE = 25
-- Height to draw the connector lines, places them just above the table and always below cards
local CONNECTION_LINE_Y = 1.529
-- we use this to turn off collision handling until onLoad() is complete
COLLISION_ENABLED = false
local collisionEnabled = false
local SHIFT_OFFSETS = {
left = { x = 0.00, y = 0, z = 7.67 },
@ -21,10 +35,23 @@ local SHIFT_EXCLUSION = {
["f182ee"] = true,
["721ba2"] = true
}
local LOC_LINK_EXCLUDE_SCENARIOS = {
["Devil Reef"] = true,
["The Witching Hour"] = true,
}
local INVESTIGATOR_COUNTER_GUID = "f182ee"
local PLAY_AREA_ZONE_GUID = "a2f932"
local clueData = {}
local spawnedLocationGUIDs = {}
local locations = { }
local locationConnections = { }
local draggingGuids = { }
local locationData
local currentScenario
---------------------------------------------------------
@ -33,17 +60,19 @@ local currentScenario
function onSave()
return JSON.encode({
currentScenario = currentScenario
trackedLocations = locations,
currentScenario = currentScenario,
})
end
function onLoad(saveState)
-- records locations we have spawned clues for
local saveData = JSON.decode(saveState) or {}
currentScenario = saveData.currentScenario
local save = JSON.decode(saveState) or { }
locations = save.trackedLocations or { }
currentScenario = save.currentScenario
self.interactable = DEBUG
Wait.time(function() COLLISION_ENABLED = true end, 1)
Wait.time(function() collisionEnabled = true end, 1)
end
function log(message)
@ -60,14 +89,15 @@ function updateLocations(args)
end
end
function onCollisionEnter(collision_info)
if not COLLISION_ENABLED then return end
function onCollisionEnter(collisionInfo)
if not collisionEnabled then return end
-- check if we should spawn clues here and do so according to playercount
local card = collision_info.collision_object
local card = collisionInfo.collision_object
if shouldSpawnTokens(card) then
tokenManager.spawnForCard(card)
end
draggingGuids[card.getGUID()] = nil
maybeTrackLocation(card)
end
function shouldSpawnTokens(card)
@ -81,6 +111,267 @@ function shouldSpawnTokens(card)
or metadata.weakness
end
function onCollisionExit(collisionInfo)
maybeUntrackLocation(collisionInfo.collision_object)
end
-- Destroyed objects don't trigger onCollisionExit(), so check on destruction to untrack as well
function onObjectDestroy(object)
maybeUntrackLocation(object)
end
function onObjectPickUp(player, object)
-- onCollisionExit fires first, so we have to check the card to see if it's a location we should
-- be tracking
if showLocationLinks() and isInPlayArea(object) and object.getGMNotes() ~= nil and object.getGMNotes() ~= "" then
local pickedUpGuid = object.getGUID()
local metadata = JSON.decode(object.getGMNotes())
if (metadata.type == "Location") then
draggingGuids[pickedUpGuid] = metadata
rebuildConnectionList()
end
end
end
function onUpdate()
-- Due to the frequence of onUpdate calls, ensure that we only process any changes to the
-- connection list once, and only redraw once
local needsConnectionRebuild = false
local needsConnectionDraw = false
for guid, _ in pairs(draggingGuids) do
local obj = getObjectFromGUID(guid)
if obj == nil or not isInPlayArea(obj) then
draggingGuids[guid] = nil
needsConnectionRebuild = true
end
-- Even if the last location left the play area, need one last draw to clear the lines
needsConnectionDraw = true
end
if (needsConnectionRebuild) then
rebuildConnectionList()
end
if needsConnectionDraw then
drawConnections()
end
end
-- Checks the given card and adds it to the list of locations tracked for connection purposes.
-- A card will be added to the tracking if it is a location in the play area (based on centerpoint).
-- @param A card object, possibly a location.
function maybeTrackLocation(card)
-- Collision checks for any part of the card overlap, but our other tracking is centerpoint
-- Ignore any collision where the centerpoint isn't in the area
if showLocationLinks() and isInPlayArea(card) then
local metadata = JSON.decode(card.getGMNotes()) or { }
if metadata.type == "Location" then
locations[card.getGUID()] = metadata
rebuildConnectionList()
drawConnections()
end
end
end
-- Stop tracking a location for connection drawing. This should be called for both collision exit
-- and destruction, as a destroyed object does not trigger collision exit. An object can also be
-- deleted mid-drag, but the ordering for drag events means we can't clear those here and those will
-- be cleared in the next onUpdate() cycle.
-- @param card Card to (maybe) stop tracking
function maybeUntrackLocation(card)
-- Locked objects no longer collide (hence triggering an exit event) but are still in the play
-- area. If the object is now locked, don't remove it.
if locations[card.getGUID()] ~= nil and not card.locked then
locations[card.getGUID()] = nil
rebuildConnectionList()
drawConnections()
end
end
-- Builds a list of GUID to GUID connection information based on the currently tracked locations.
-- This will update the connection information and store it in the locationConnections data member,
-- but does not draw those connections. This should often be followed by a call to
-- drawConnections()
function rebuildConnectionList()
if not showLocationLinks() then
locationConnections = { }
return
end
local iconCardList = { }
-- Build a list of cards with each icon as their location ID
for cardId, metadata in pairs(draggingGuids) do
buildLocListByIcon(cardId, iconCardList)
end
for cardId, metadata in pairs(locations) do
buildLocListByIcon(cardId, iconCardList)
end
-- Pair up all the icons
locationConnections = { }
for cardId, metadata in pairs(draggingGuids) do
buildConnection(cardId, iconCardList)
end
for cardId, metadata in pairs(locations) do
if draggingGuids[cardId] == nil then
buildConnection(cardId, iconCardList)
end
end
end
-- Extracts the card's icon string into a list of individual location icons
-- @param cardID GUID of the card to pull the icon data from
-- @param iconCardList A table of icon->GUID list. Mutable, will be updated by this method
function buildLocListByIcon(cardId, iconCardList)
local card = getObjectFromGUID(cardId)
local locData = getLocationData(card)
if locData ~= nil and locData.icons ~= nil then
for icon in string.gmatch(locData.icons, "%a+") do
if iconCardList[icon] == nil then
iconCardList[icon] = { }
end
table.insert(iconCardList[icon], card.getGUID())
end
end
end
-- Builds the connections for the given cardID by finding matching icons and adding them to the
-- Playarea's locationConnections table.
-- @param cardId GUID of the card to build the connections for
-- @param iconCardList A table of icon->GUID List. Used to find matching icons for connections.
function buildConnection(cardId, iconCardList)
local card = getObjectFromGUID(cardId)
local locData = getLocationData(card)
if locData ~= nil and locData.connections ~= nil then
locationConnections[card.getGUID()] = { }
for icon in string.gmatch(locData.connections, "%a+") do
if iconCardList[icon] ~= nil then
for _, connectedGuid in ipairs(iconCardList[icon]) do
-- If the reciprocal exists, convert it to BiDi, otherwise add as a one-way
if locationConnections[connectedGuid] ~= nil
and locationConnections[connectedGuid][card.getGUID()] ~= nil then
locationConnections[connectedGuid][card.getGUID()] = BIDIRECTIONAL
else
locationConnections[card.getGUID()][connectedGuid] = ONE_WAY
end
end
end
end
end
end
-- Helper method to extract the location metadata from a card based on whether it's front or back
-- is showing.
-- @param card Card object to extract data from
-- @return Table with either the locationFront or locationBack metadata structure, or nil if the
-- metadata doesn't exist
function getLocationData(card)
if card == nil then
return nil
end
if card.is_face_down then
return JSON.decode(card.getGMNotes()).locationBack
else
return JSON.decode(card.getGMNotes()).locationFront
end
end
-- Draws the lines for connections currently in locationConnections.
function drawConnections()
if not showLocationLinks() then
locationConnections = { }
return
end
local cardConnectionLines = { }
for originGuid, targetGuids in pairs(locationConnections) do
-- Objects should reliably exist at this point, but since this can be called during onUpdate the
-- object checks are conservative just to make sure.
local origin = getObjectFromGUID(originGuid)
if origin != nil then
for targetGuid, direction in pairs(targetGuids) do
local target = getObjectFromGUID(targetGuid)
if target != nil then
if direction == BIDIRECTIONAL then
addBidirectionalVector(origin, target, cardConnectionLines)
elseif direction == ONE_WAY then
addOneWayVector(origin, target, cardConnectionLines)
end
end
end
end
end
self.setVectorLines(cardConnectionLines)
end
-- Draws a bidirectional location connection between the two cards, adding the lines to do so to the
-- given lines list.
-- @param card1 One of the card objects to connect
-- @param card2 The other card object to connect
-- @param lines List of vector line elements. Mutable, will be updated to add this connector
function addBidirectionalVector(card1, card2, lines)
local cardPos1 = card1.getPosition()
local cardPos2 = card2.getPosition()
cardPos1.y = CONNECTION_LINE_Y
cardPos2.y = CONNECTION_LINE_Y
local pos1 = self.positionToLocal(cardPos1)
local pos2 = self.positionToLocal(cardPos2)
table.insert(lines, {
points = { pos1, pos2 },
color = CONNECTION_COLOR,
thickness = CONNECTION_THICKNESS,
})
end
-- Draws a one-way location connection between the two cards, adding the lines to do so to the
-- given lines list. Arrows will point towards the target card.
-- @param origin Origin card in the connection
-- @param target Target card object to connect
-- @param lines List of vector line elements. Mutable, will be updated to add this connector
function addOneWayVector(origin, target, lines)
-- Start with the BiDi then add the arrow lines to it
addBidirectionalVector(origin, target, lines)
local originPos = origin.getPosition()
local targetPos = target.getPosition()
originPos.y = CONNECTION_LINE_Y
targetPos.y = CONNECTION_LINE_Y
-- Calculate card distance to be closer for horizontal positions than vertical, since cards are
-- taller than they are wide
local heading = Vector(originPos):sub(targetPos):heading("y")
local distanceFromCard = DIRECTIONAL_ARROW_DISTANCE * 0.7 + DIRECTIONAL_ARROW_DISTANCE * 0.3 * math.abs(math.sin(math.rad(heading)))
-- Calculate the three possible arrow positions. These are offset by half the arrow length to
-- make them visually balanced by keeping the arrows centered, not tracking the point
local midpoint = Vector(originPos):add(targetPos):scale(Vector(0.5, 0.5, 0.5)):moveTowards(targetPos, ARROW_ARM_LENGTH / 2)
local closeToOrigin = Vector(originPos):moveTowards(targetPos, distanceFromCard + ARROW_ARM_LENGTH / 2)
local closeToTarget = Vector(targetPos):moveTowards(originPos, distanceFromCard - ARROW_ARM_LENGTH / 2)
if (originPos:distance(closeToOrigin) > originPos:distance(closeToTarget)) then
addArrowLines(midpoint, originPos, lines)
else
addArrowLines(closeToOrigin, originPos, lines)
addArrowLines(closeToTarget, originPos, lines)
end
end
-- Draws an arrowhead at the given position.
-- @param arrowheadPosition Centerpoint of the arrowhead to draw (NOT the tip of the arrow)
-- @param originPos Origin point of the connection, used to position the arrow arms
-- @param lines List of vector line elements. Mutable, will be updated to add this arrow
function addArrowLines(arrowheadPos, originPos, lines)
local arrowArm1 = Vector(arrowheadPos):moveTowards(originPos, ARROW_ARM_LENGTH):sub(arrowheadPos):rotateOver("y", -1 * ARROW_ANGLE):add(arrowheadPos)
local arrowArm2 = Vector(arrowheadPos):moveTowards(originPos, ARROW_ARM_LENGTH):sub(arrowheadPos):rotateOver("y", ARROW_ANGLE):add(arrowheadPos)
local head = self.positionToLocal(arrowheadPos)
local arm1 = self.positionToLocal(arrowArm1)
local arm2 = self.positionToLocal(arrowArm2)
table.insert(lines, {
points = { arm1, head, arm2},
color = CONNECTION_COLOR,
thickness = CONNECTION_THICKNESS
})
end
-- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain
-- fixed objects will be ignored, as will anything the player has tagged with
-- 'displacement_excluded'
@ -123,6 +414,20 @@ function getInvestigatorCount()
return investigatorCounter.getVar("val")
end
-- Check to see if the given object is within the bounds of the play area, based solely on the X and
-- Z coordinates, ignoring height
-- @param object Object to check
-- @return True if the object is inside the play area
function isInPlayArea(object)
local bounds = self.getBounds()
local position = object.getPosition()
-- Corners are arbitrary since it's all global - c1 goes down both axes, c2 goes up
local c1 = { x = bounds.center.x - bounds.size.x / 2, z = bounds.center.z - bounds.size.z / 2}
local c2 = { x = bounds.center.x + bounds.size.x / 2, z = bounds.center.z + bounds.size.z / 2}
return position.x > c1.x and position.x < c2.x and position.z > c1.z and position.z < c2.z
end
-- Reset the play area's tracking of which cards have had tokens spawned.
function resetSpawnedCards()
spawnedLocationGUIDs = {}
@ -130,4 +435,11 @@ end
function onScenarioChanged(scenarioName)
currentScenario = scenarioName
if not showLocationLinks() then
broadcastToAll("Automatic location connections not available for this scenario")
end
end
function showLocationLinks()
return not LOC_LINK_EXCLUDE_SCENARIOS[currentScenario]
end

View File

@ -248,6 +248,8 @@ do
else
rot.y = 270
end
tokenTemplate.Nickname = ""
return spawnObjectData({
data = tokenTemplate,
position = position,

View File

@ -6,8 +6,8 @@ tourCardTemplate = {
tag = "Panel",
attributes = {
id = "tourCard",
height = 195,
width = 300,
height = 215,
width = 330,
rotation = "0 0 0",
position = "0 300 30",
showAnimation = "FadeIn",
@ -19,10 +19,10 @@ tourCardTemplate = {
tag = "Image",
attributes = {
id = "tourNarratorImageLeft",
height=75,
width=50,
height=120,
width=80,
rectAlignment="UpperLeft",
offsetXY = "-50 0",
offsetXY = "-80 0",
-- Image will be set when the card is updated
}
},
@ -31,10 +31,10 @@ tourCardTemplate = {
attributes = {
id = "tourNarratorImageRight",
active = false,
height=75,
width=50,
height=125,
width=80,
rectAlignment="UpperRight",
offsetXY = "50 0"
offsetXY = "80 0"
-- Image will be set when the card is updated
}
},
@ -43,8 +43,8 @@ tourCardTemplate = {
attributes = {
id = "tourSpeechBubble",
color = "#F5F5DC",
height = 195,
width = 300,
height = 215,
width = 330,
rectAlignment = "MiddleCenter",
image = "SpeechBubble",
},
@ -53,35 +53,37 @@ tourCardTemplate = {
tag = "Text",
attributes = {
id = "tourText",
height = 165,
width = 230,
-- Everything on this is double-sized and scaled down to keep the text sharps
height = 370,
width = 520,
scale = "0.5 0.5 1",
rectAlignment = "UpperCenter",
offsetXY = "15 -15",
resizeTextForBestFit = true,
resizeTextMinSize = 10,
resizeTextMaxSize = 16,
resizeTextMinSize = 20,
resizeTextMaxSize = 32,
color = "#050505",
alignment = "UpperLeft",
horizontalOverflow = "wrap",
}
},
{
tag = "Button",
tag = "Image",
attributes = {
id = "tourNext",
height = 40,
width = 40,
height = 45,
width = 45,
rectAlignment = "LowerRight",
offsetXY = "-5 -45",
image = "NextArrow"
},
},
{
tag = "Button",
tag = "Image",
attributes = {
id = "tourStop",
height = 40,
width = 40,
height = 45,
width = 45,
rectAlignment = "LowerLeft",
offsetXY = "35 -45",
image = "Exit"

View File

@ -36,10 +36,14 @@ do
east = "600 0 0",
west = "-600 0 0",
south = "0 -300 0",
northwest = "-600 300 0",
-- Northwest is only used by the Mandy card, move it a little right than standard so it's
-- closer to the importer
northwest = "-500 300 0",
northeast = "600 300 0",
southwest = "-600 -300 0",
southeast = "600 -300 0"
-- Used by the Diana and Wini cards referencing the bottom-right global controls, moved a little
-- closer to them
southeast = "730 -365 0"
}
-- Tracks the current state of the tours. Keyed by player color to keep each player's tour
@ -264,7 +268,6 @@ do
---@param playerColor String. String color of the player to make this visible for
internal.setDeepVisibility = function(xmlUi, playerColor)
xmlUi.attributes.visibility = "" .. playerColor
log(xmlUi.attributes.id)
if xmlUi.children ~= nil then
for _, child in ipairs(xmlUi.children) do
internal.setDeepVisibility(child, playerColor)

View File

@ -8,7 +8,7 @@ TOUR_SCRIPT = {
},
{
narrator = "Darrell",
text = "Cameras can be tricky things. Best you leave handling it to the professionals during the tour. Don't try to move the camera until the tour is complete.\n\nOnce we're done, remmeber you can use the 'p' key to switch back to third-person mode, and the spacebar to reset the position.",
text = "Cameras can be tricky things. Best you leave handling it to the professionals during the tour. Don't try to move the camera until the tour is complete.\n\nOnce we're done, remember you can use the 'p' key to switch back to third-person mode, and the spacebar to reset the position.",
position = "center",
speakerSide = "right",
},
@ -25,7 +25,7 @@ TOUR_SCRIPT = {
text = "To survive what's coming you'll need a deck. If it's safely hidden away on ArkhamDB you can load it here, and even find the newest version after an upgrade without changing the ID.\n\nNo need to publish all your decks, use 'Private' and you can see it. Just make sure to select 'Make your decks public' in ArkhamDB.",
showObj = "a28140",
distanceFromObj = -10,
position = "west",
position = "northwest",
},
{
narrator = "Daniela",
@ -83,7 +83,7 @@ TOUR_SCRIPT = {
},
{
narrator = "Preston",
text = "I can afford to buy what I need, but for those less well-off we've provided an endless pool of tokens to track your game. Simply drag one out of the pools here.\n\nResources are my favorite, of course, but damage and horror are as inevitable as taxes, though I leave those to my bookkeeper. Those tokens can work like counters, use the number keys to change the value.",
text = "I can afford to buy what I need, but for those less well-off we've provided an endless pool of tokens to track your game. Simply drag one out of the pools here.\n\nResources are my favorite of course, but damage and horror are as inevitable as taxes. I leave those to my bookkeeper though. Those tokens can work like counters, use the number keys to change the value.",
showObj = "9fadf9",
position = "north",
skipCentering = true,

View File

@ -26,7 +26,6 @@ function onLoad()
end
function startTour(_, playerColor, _)
broadcastToColor("Starting the tour, please wait", playerColor)
tourManager.startTour(playerColor)
end

View File

@ -7,6 +7,8 @@ local WEAKNESS_CHECK_Z = 37
local cardIdIndex = { }
local classAndLevelIndex = { }
local basicWeaknessList = { }
local uniqueWeaknessList = { }
local cycleIndex = { }
local indexingDone = false
local allowRemoval = false
@ -45,7 +47,9 @@ function clearIndexes()
classAndLevelIndex["Survivor-level0"] = { }
classAndLevelIndex["Rogue-level0"] = { }
classAndLevelIndex["Neutral-level0"] = { }
cycleIndex = { }
basicWeaknessList = { }
uniqueWeaknessList = { }
end
-- Clears the bag indexes and starts the coroutine to rebuild the indexes
@ -121,27 +125,27 @@ function buildSupplementalIndexes()
local cardMetadata = card.metadata
-- If the ID key and the metadata ID don't match this is a duplicate card created by an
-- alternate_id, and we should skip it
if (cardId == cardMetadata.id) then
if cardId == cardMetadata.id then
-- Add card to the basic weakness list, if appropriate. Some weaknesses have
-- multiple copies, and are added multiple times
if (cardMetadata.weakness and cardMetadata.basicWeaknessCount ~= nil) then
if cardMetadata.weakness and cardMetadata.basicWeaknessCount ~= nil then
table.insert(uniqueWeaknessList, cardMetadata.id)
for i = 1, cardMetadata.basicWeaknessCount do
table.insert(basicWeaknessList, cardMetadata.id)
end
end
end
table.sort(basicWeaknessList, cardComparator)
-- 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
-- Excludes signature cards (which have no class or level) and alternate
-- ID entries
if (cardMetadata.class ~= nil and cardMetadata.level ~= nil) then
-- 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
-- Excludes signature cards (which have no class or level) and alternate
-- ID entries
if (cardMetadata.class ~= nil and cardMetadata.level ~= nil) then
isGuardian = string.match(cardMetadata.class, "Guardian")
isSeeker = string.match(cardMetadata.class, "Seeker")
isMystic = string.match(cardMetadata.class, "Mystic")
@ -171,12 +175,32 @@ function buildSupplementalIndexes()
if (isNeutral) then
table.insert(classAndLevelIndex["Neutral"..upgradeKey], cardMetadata.id)
end
local cycleName = cardMetadata.cycle
if cycleName ~= nil then
cycleName = string.lower(cycleName)
if string.match(cycleName, "return") then
cycleName = string.sub(cycleName, 11)
end
if cycleName == "the night of the zealot" then
cycleName = "core"
end
if cycleIndex[cycleName] == nil then
cycleIndex[cycleName] = { }
end
table.insert(cycleIndex[cycleName], cardMetadata.id)
end
end
end
end
for _, indexTable in pairs(classAndLevelIndex) do
table.sort(indexTable, cardComparator)
end
for _, indexTable in pairs(cycleIndex) do
table.sort(indexTable)
end
table.sort(basicWeaknessList, cardComparator)
table.sort(uniqueWeaknessList, cardComparator)
end
-- Comparison function used to sort the class card bag indexes. Sorts by card
@ -235,6 +259,14 @@ function getCardsByClassAndLevel(params)
return classAndLevelIndex[params.class..upgradeKey];
end
function getCardsByCycle(cycleName)
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 cycleIndex[string.lower(cycleName)]
end
-- Searches the bag for cards which match the given name and returns a list. Note that this is
-- an O(n) search without index support. It may be slow.
-- Parameter array must contain these fields to define the search:
@ -303,6 +335,10 @@ function getBasicWeaknesses()
return basicWeaknessList
end
function getUniqueWeaknesses()
return uniqueWeaknessList
end
-- Helper function that adds one to the table entry for the number of weaknesses in play
function incrementWeaknessCount(table, cardMetadata)
if (isBasicWeakness(cardMetadata)) then
@ -322,6 +358,7 @@ function isInPlayArea(object)
return position.x < WEAKNESS_CHECK_X
and position.z < WEAKNESS_CHECK_Z
end
function isBasicWeakness(cardMetadata)
return cardMetadata ~= nil
and cardMetadata.weakness

View File

@ -2,24 +2,49 @@ require("playercards/PlayerCardPanelData")
local spawnBag = require("playercards/spawnbag/SpawnBag")
local arkhamDb = require("arkhamdb/ArkhamDb")
-- TODO: Update when the real UI image is in place
local BUTTON_WIDTH = 150
local BUTTON_HEIGHT = 550
-- Size and position information for the three rows of class buttons
local CIRCLE_BUTTON_SIZE = 250
local CLASS_BUTTONS_X_OFFSET = 0.1325
local INVESTIGATOR_ROW_START = Vector(0.125, 0.1, -0.447)
local LEVEL_ZERO_ROW_START = Vector(0.125, 0.1, -0.007)
local UPGRADED_ROW_START = Vector(0.125, 0.1, 0.333)
-- Size and position information for the two blocks of other buttons
local MISC_BUTTONS_X_OFFSET = 0.155
local WEAKNESS_ROW_START = Vector(0.157, 0.1, 0.666)
local OTHER_ROW_START = Vector(0.605, 0.1, 0.666)
-- Size and position information for the Cycle (box) buttons
local CYCLE_BUTTON_SIZE = 468
local CYCLE_BUTTON_START = Vector(-0.716, 0.1, -0.39)
local CYCLE_COLUMN_COUNT = 3
local CYCLE_BUTTONS_X_OFFSET = 0.267
local CYCLE_BUTTONS_Z_OFFSET = 0.2665
local ALL_CARDS_BAG_GUID = "15bb07"
local STARTER_DECK_MODE_SELECTED_COLOR = { 0.2, 0.2, 0.2, 0.8 }
local TRANSPARENT = { 0, 0, 0, 0 }
local STARTER_DECK_MODE_STARTERS = "starters"
local STARTER_DECK_MODE_CARDS_ONLY = "cards"
local FACE_UP_ROTATION = { x = 0, y = 270, z = 0}
local FACE_DOWN_ROTATION = { x = 0, y = 270, z = 180}
-- Coordinates to begin laying out cards to match the reserved areas of the
-- table. Cards will lay out horizontally, then create additional rows
-- Coordinates to begin laying out cards. These vary based on the cards that are being placed
local START_POSITIONS = {
skill = Vector(58.384, 1.36, 92.4),
event = Vector(53.229, 1.36, 92.4),
asset = Vector(40.960, 1.36, 92.4),
investigator = Vector(60, 1.36, 80)
classCards = Vector(58.384, 1.36, 92.4),
investigator = Vector(60, 1.36, 86),
cycle = Vector(48, 1.36, 92.4),
other = Vector(56, 1.36, 86),
summonedServitor = Vector(55.5, 1.36, 60.2),
randomWeakness = Vector(55, 1.36, 75)
}
-- Shifts to move rows of cards, and groups of rows, as different groupings are laid out
local CARD_ROW_OFFSET = 3.7
local CARD_GROUP_OFFSET = 2
-- Position offsets for investigator decks in investigator mode, defines the spacing for how the
-- rows and columns are laid out
local INVESTIGATOR_POSITION_SHIFT_ROW = Vector(-11, 0, 0)
@ -31,7 +56,21 @@ local INVESTIGATOR_MAX_COLS = 6
local INVESTIGATOR_CARD_OFFSET = Vector(-2.55, 0, 0)
local INVESTIGATOR_SIGNATURE_OFFSET = Vector(-5.75, 0, 0)
local spawnStarterDecks = false
local CLASS_LIST = { "Guardian", "Seeker", "Rogue", "Mystic", "Survivor", "Neutral" }
local CYCLE_LIST = {
"Core",
"The Dunwich Legacy",
"The Path to Carcosa",
"The Forgotten Age",
"The Circle Undone",
"The Dream-Eaters",
"The Innsmouth Conspiracy",
"Edge of the Earth",
"The Scarlet Keys",
"Investigator Packs"
}
local starterDeckMode = STARTER_DECK_MODE_CARDS_ONLY
function onSave()
local saveState = {
@ -48,232 +87,223 @@ function onLoad(savedData)
spawnBag.loadFromSave(saveState.spawnBagState)
end
end
createButtons()
end
self.createButton({
label="Guardian", click_function="spawnInvestigatorsGuardian", function_owner=self,
position={-0.3,0.2,-0.5}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Seeker", click_function="spawnInvestigatorsSeeker", function_owner=self,
position={0,0.2,-0.5}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Mystic", click_function="spawnInvestigatorsMystic", function_owner=self,
position={0.3,0.2,-0.5}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Rogue", click_function="spawnInvestigatorsRogue", function_owner=self,
position={-0.3,0.2,-0.4}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Survivor", click_function="spawnSurvivor", function_owner=self,
position={0,0.2,-0.4}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Neutral", click_function="spawnNeutral", function_owner=self,
position={0.3,0.2,-0.4}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
function createButtons()
createInvestigatorButtons()
createLevelZeroButtons()
createUpgradedButtons()
createWeaknessButtons()
createOtherButtons()
createCycleButtons()
createClearButton()
-- Create investigator mode buttons last so the indexes are set when we need to update them
createInvestigatorModeButtons()
end
self.createButton({
label="Core", click_function="spawnCore", function_owner=self,
position={-0.3,0.2,-0.2}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Dunwich", click_function="spawnDunwich", function_owner=self,
position={0,0.2,-0.2}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Carcosa", click_function="spawnCarcosa", function_owner=self,
position={0.3,0.2,-0.2}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Forgotten Age", click_function="spawnForgottenAge", function_owner=self,
position={-0.3,0.2,-0.1}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Circle Undone", click_function="spawnCircleUndone", function_owner=self,
position={0,0.2,-0.1}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Dream Eaters", click_function="spawnDreamEaters", function_owner=self,
position={0.3,0.2,-0.1}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Innsmouth", click_function="spawnInnsmouth", function_owner=self,
position={-0.3,0.2,0}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="EotE", click_function="spawnEotE", function_owner=self,
position={0,0.2,0}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Scarlet Keys", click_function="spawnScarletKeys", function_owner=self,
position={0.3,0.2,0}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="InvPacks", click_function="spawnInvestigatorDecks", function_owner=self,
position={-0.3,0.2,0.1}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Investigators", click_function="setInvestigators", function_owner=self,
position={-0.15,0.2,-0.6}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Starters", click_function="setStarters", function_owner=self,
position={0.15,0.2,-0.6}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="L0 Guardian", click_function="spawnBasicGuardian", function_owner=self,
position={-0.15,0.2,0.3}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="L1-5 Guardian", click_function="spawnUpgradedGuardian", function_owner=self,
position={0.15,0.2,0.3}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="L0 Seeker", click_function="spawnBasicSeeker", function_owner=self,
position={-0.15,0.2,0.4}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="L1-5 Seeker", click_function="spawnUpgradedSeeker", function_owner=self,
position={0.15,0.2,0.4}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="L0 Mystic", click_function="spawnBasicMystic", function_owner=self,
position={-0.15,0.2,0.5}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="L1-5 Mystic", click_function="spawnUpgradedGuardian", function_owner=self,
position={0.15,0.2,0.5}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="L0 Rogue", click_function="spawnBasicRogue", function_owner=self,
position={-0.15,0.2,0.6}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="L1-5 Rogue", click_function="spawnUpgradedRogue", function_owner=self,
position={0.15,0.2,0.6}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="L0 Survivor", click_function="spawnBasicSurvivor", function_owner=self,
position={-0.15,0.2,0.7}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="L1-5 Survivor", click_function="spawnUpgradedSurvivor", function_owner=self,
position={0.15,0.2,0.7}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="L0 Neutral", click_function="spawnBasicNeutral", function_owner=self,
position={-0.15,0.2,0.8}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="L1-5 Neutral", click_function="spawnUpgradedNeutral", function_owner=self,
position={0.15,0.2,0.8}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Clear", click_function="deleteAll", function_owner=self,
position={0.5,0.2,0.9}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
self.createButton({
label="Weaknesses", click_function="spawnWeaknesses", function_owner=self,
position={-0.5,0.2,0.9}, rotation={0,0,0}, height=BUTTON_WIDTH, width=BUTTON_HEIGHT,
font_size=64, color={0,0,0}, font_color={1,1,1}, scale={0.25, 0.25, 0.25}
})
local classList = { "Guardian", "Seeker", "Mystic", "Rogue", "Survivor", "Neutral" }
for _, className in ipairs(classList) do
local funcName = "spawnInvestigators"..className
self.setVar(funcName, function(_, _, _) spawnGroup(className) end)
funcName = "spawnBasic"..className
self.setVar(funcName, function(_, _, _) spawnClassCards(className, false) end)
funcName = "spawnUpgraded"..className
self.setVar(funcName, function(_, _, _) spawnClassCards(className, true) end)
function createInvestigatorButtons()
local invButtonParams = {
function_owner = self,
rotation = Vector(0, 0, 0),
height = CIRCLE_BUTTON_SIZE,
width = CIRCLE_BUTTON_SIZE,
scale = Vector(0.25, 1, 0.25),
color = TRANSPARENT,
}
local buttonPos = INVESTIGATOR_ROW_START:copy()
for _, class in ipairs(CLASS_LIST) do
invButtonParams.click_function = "spawnInvestigators" .. class
invButtonParams.position = buttonPos
self.createButton(invButtonParams)
buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET
self.setVar(invButtonParams.click_function, function(_, _, _) spawnInvestigatorGroup(class) end)
end
end
-- TODO: Replace these with something less manual once the full data in in place so we know what
-- keys to use
function placeCore()
spawnGroup("Core")
function createLevelZeroButtons()
local l0ButtonParams = {
function_owner = self,
rotation = Vector(0, 0, 0),
height = CIRCLE_BUTTON_SIZE,
width = CIRCLE_BUTTON_SIZE,
scale = Vector(0.25, 1, 0.25),
color = TRANSPARENT,
}
local buttonPos = LEVEL_ZERO_ROW_START:copy()
for _, class in ipairs(CLASS_LIST) do
l0ButtonParams.click_function = "spawnBasic" .. class
l0ButtonParams.position = buttonPos
self.createButton(l0ButtonParams)
buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET
self.setVar(l0ButtonParams.click_function, function(_, _, _) spawnClassCards(class, false) end)
end
end
function placeDunwich()
spawnGroup("Dunwich")
function createUpgradedButtons()
local upgradedButtonParams = {
function_owner = self,
rotation = Vector(0, 0, 0),
height = CIRCLE_BUTTON_SIZE,
width = CIRCLE_BUTTON_SIZE,
scale = Vector(0.25, 1, 0.25),
color = TRANSPARENT,
}
local buttonPos = UPGRADED_ROW_START:copy()
for _, class in ipairs(CLASS_LIST) do
upgradedButtonParams.click_function = "spawnUpgraded" .. class
upgradedButtonParams.position = buttonPos
self.createButton(upgradedButtonParams)
buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET
self.setVar(upgradedButtonParams.click_function, function(_, _, _) spawnClassCards(class, true) end)
end
end
function placeCarcosa()
spawnGroup("Carcosa")
function createWeaknessButtons()
local weaknessButtonParams = {
function_owner = self,
rotation = Vector(0, 0, 0),
height = CIRCLE_BUTTON_SIZE,
width = CIRCLE_BUTTON_SIZE,
scale = Vector(0.25, 1, 0.25),
color = TRANSPARENT,
}
local buttonPos = WEAKNESS_ROW_START:copy()
weaknessButtonParams.click_function = "spawnWeaknesses"
weaknessButtonParams.tooltip = "Basic Weaknesses"
weaknessButtonParams.position = buttonPos
self.createButton(weaknessButtonParams)
buttonPos.x = buttonPos.x + MISC_BUTTONS_X_OFFSET
weaknessButtonParams.click_function = "spawnRandomWeakness"
weaknessButtonParams.tooltip = "Random Weakness"
weaknessButtonParams.position = buttonPos
self.createButton(weaknessButtonParams)
end
function placeForgottenAge()
spawnGroup("ForgottenAge")
function createOtherButtons()
local otherButtonParams = {
function_owner = self,
rotation = Vector(0, 0, 0),
height = CIRCLE_BUTTON_SIZE,
width = CIRCLE_BUTTON_SIZE,
scale = Vector(0.25, 1, 0.25),
color = TRANSPARENT,
}
local buttonPos = OTHER_ROW_START:copy()
otherButtonParams.click_function = "spawnBonded"
otherButtonParams.tooltip = "Bonded Cards"
otherButtonParams.position = buttonPos
self.createButton(otherButtonParams)
buttonPos.x = buttonPos.x + MISC_BUTTONS_X_OFFSET
otherButtonParams.click_function = "spawnUpgradeSheets"
otherButtonParams.tooltip = "Customization Upgrade Sheets"
otherButtonParams.position = buttonPos
self.createButton(otherButtonParams)
end
function placeCircleUndone()
spawnGroup("CircleUndone")
function createCycleButtons()
local cycleButtonParams = {
function_owner = self,
rotation = Vector(0, 0, 0),
height = CYCLE_BUTTON_SIZE,
width = CYCLE_BUTTON_SIZE,
scale = Vector(0.25, 1, 0.25),
color = TRANSPARENT,
}
local buttonPos = CYCLE_BUTTON_START:copy()
local rowCount = 0
local colCount = 0
for _, cycle in ipairs(CYCLE_LIST) do
cycleButtonParams.click_function = "spawnCycle" .. cycle
cycleButtonParams.position = buttonPos
cycleButtonParams.tooltip = cycle
self.createButton(cycleButtonParams)
self.setVar(cycleButtonParams.click_function, function(_, _, _) spawnCycle(cycle) end)
colCount = colCount + 1
-- If we've reached the end of a row, shift down and back to the first column
if colCount >= CYCLE_COLUMN_COUNT then
buttonPos = CYCLE_BUTTON_START:copy()
rowCount = rowCount + 1
colCount = 0
buttonPos.z = buttonPos.z + CYCLE_BUTTONS_Z_OFFSET * rowCount
if rowCount == 3 then
-- Account for centered button on the final row
buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET
end
else
buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET
end
end
end
function placeDreamEaters()
spawnGroup("DreamEaters")
function createClearButton()
self.createButton({
function_owner = self,
click_function = "deleteAll",
position = Vector(0, 0.1, 0.852),
rotation = Vector(0, 0, 0),
height = 170,
width = 750,
scale = Vector(0.25, 1, 0.25),
color = TRANSPARENT,
})
end
function placeInnsmouth()
spawnGroup("Innsmouth")
function createInvestigatorModeButtons()
local starterMode = starterDeckMode == STARTER_DECK_MODE_STARTERS
self.createButton({
function_owner = self,
click_function = "setCardsOnlyMode",
position = Vector(0.251, 0.1, -0.322),
rotation = Vector(0, 0, 0),
height = 170,
width = 760,
scale = Vector(0.25, 1, 0.25),
color = starterMode and TRANSPARENT or STARTER_DECK_MODE_SELECTED_COLOR
})
self.createButton({
function_owner = self,
click_function = "setStarterDeckMode",
position = Vector(0.66, 0.1, -0.322),
rotation = Vector(0, 0, 0),
height = 170,
width = 760,
scale = Vector(0.25, 1, 0.25),
color = starterMode and STARTER_DECK_MODE_SELECTED_COLOR or TRANSPARENT
})
local checkX = starterMode and 0.52 or 0.11
self.createButton({
function_owner = self,
label = "✓",
click_function = "doNothing",
position = Vector(checkX, 0.11, -0.317),
rotation = Vector(0, 0, 0),
height = 0,
width = 0,
scale = Vector(0.3, 1, 0.3),
font_color = { 0, 0, 0 },
color = { 1, 1, 1 }
})
end
function placeEotE()
spawnGroup("EotE")
function setStarterDeckMode()
starterDeckMode = STARTER_DECK_MODE_STARTERS
updateStarterModeButtons()
end
function placeScarletKeys()
spawnGroup("ScarletKeys")
function setCardsOnlyMode()
starterDeckMode = STARTER_DECK_MODE_CARDS_ONLY
updateStarterModeButtons()
end
function placeInvestigatorDecks()
spawnGroup("InvestigatorDecks")
end
-- UI handler to put the investigator spawn in investigator mode.
function setInvestigators()
spawnStarterDecks = false
printToAll("Spawning investigator piles")
end
-- UI handler to put the investigator spawn in starter deck mode.
function setStarters()
spawnStarterDecks = true
printToAll("Spawning starter decks")
function updateStarterModeButtons()
local buttonCount = #self.getButtons()
-- Buttons are 0-indexed, so the last three are -1, -2, and -3 from the size
self.removeButton(buttonCount - 1)
self.removeButton(buttonCount - 2)
self.removeButton(buttonCount - 3)
createInvestigatorModeButtons()
end
-- Deletes all cards currently placed on the table
@ -284,10 +314,11 @@ end
-- Spawn an investigator group, based on the current UI setting for either investigators or starter
-- decks.
---@param groupName String. Name of the group to spawn, matching a key in InvestigatorPanelData
function spawnGroup(groupName)
function spawnInvestigatorGroup(groupName)
local starterMode = starterDeckMode == STARTER_DECK_MODE_STARTERS
spawnBag.recall(true)
Wait.frames(function()
if spawnStarterDecks then
if starterMode then
spawnStarters(groupName)
else
spawnInvestigators(groupName)
@ -359,13 +390,13 @@ function buildCommonSpawnSpec(investigatorName, investigatorData, position, oneC
return {
{
name = investigatorName.."minicards",
cards = oneCardOnly and investigatorData.minicards[1] or investigatorData.minicards,
cards = oneCardOnly and { investigatorData.minicards[1] } or investigatorData.minicards,
globalPos = position,
rotation = FACE_UP_ROTATION,
},
{
name = investigatorName.."cards",
cards = oneCardOnly and investigatorData.cards[1] or investigatorData.cards,
cards = oneCardOnly and { investigatorData.cards[1] } or investigatorData.cards,
globalPos = cardPos,
rotation = FACE_UP_ROTATION,
},
@ -452,31 +483,34 @@ function placeClassCards(cardClass, isUpgraded)
table.insert(assetList, cardId)
end
end
local groupPos = Vector(START_POSITIONS.classCards)
if #skillList > 0 then
spawnBag.spawn({
name = cardClass .. (isUpgraded and "upgraded" or "basic"),
cards = skillList,
globalPos = START_POSITIONS.skill,
globalPos = groupPos,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
groupPos.x = groupPos.x - math.ceil(#skillList / 20) * CARD_ROW_OFFSET - CARD_GROUP_OFFSET
end
if #eventList > 0 then
spawnBag.spawn({
name = cardClass .. "event" .. (isUpgraded and "upgraded" or "basic"),
cards = eventList,
globalPos = START_POSITIONS.event,
globalPos = groupPos,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
groupPos.x = groupPos.x - math.ceil(#eventList / 20) * CARD_ROW_OFFSET - CARD_GROUP_OFFSET
end
if #assetList > 0 then
spawnBag.spawn({
name = cardClass .. "asset" .. (isUpgraded and "upgraded" or "basic"),
cards = assetList,
globalPos = START_POSITIONS.asset,
globalPos = groupPos,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
@ -484,26 +518,108 @@ function placeClassCards(cardClass, isUpgraded)
end
end
-- Clears the current cards, and places all basic weaknesses on the table.
function spawnWeaknesses()
spawnBag.recall(fast)
-- Spawns the investigator sets and all cards for the given cycle
---@param cycle String Name of a cycle, should match the standard used in card metadata
function spawnCycle(cycle)
spawnBag.recall(true)
spawnInvestigators(cycle)
local allCardsBag = getObjectFromGUID(ALL_CARDS_BAG_GUID)
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
local weaknessIdList = allCardsBag.call("getBasicWeaknesses")
local cycleCardList = allCardsBag.call("getCardsByCycle", cycle)
local copiedList = { }
for i, id in ipairs(weaknessIdList) do
for i, id in ipairs(cycleCardList) do
copiedList[i] = id
end
spawnBag.spawn({
name = "weaknesses",
name = "cycle"..cycle,
cards = copiedList,
globalPos = START_POSITIONS.asset,
globalPos = START_POSITIONS.cycle,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
end
function spawnBonded()
spawnBag.recall(true)
spawnBag.spawn({
name = "bonded",
cards = BONDED_CARD_LIST,
globalPos = START_POSITIONS.classCards,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
end
function spawnUpgradeSheets()
spawnBag.recall(true)
spawnBag.spawn({
name = "upgradeSheets",
cards = UPGRADE_SHEET_LIST,
globalPos = START_POSITIONS.classCards,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
spawnBag.spawn({
name = "servitor",
cards = { "09080-m" },
globalPos = START_POSITIONS.summonedServitor,
rotation = FACE_UP_ROTATION,
})
end
-- Clears the current cards, and places all basic weaknesses on the table.
function spawnWeaknesses()
spawnBag.recall(true)
local allCardsBag = getObjectFromGUID(ALL_CARDS_BAG_GUID)
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
local weaknessIdList = allCardsBag.call("getUniqueWeaknesses")
local copiedList = { }
for i, id in ipairs(weaknessIdList) do
copiedList[i] = id
end
local groupPos = Vector(START_POSITIONS.classCards)
spawnBag.spawn({
name = "weaknesses",
cards = copiedList,
globalPos = groupPos,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
groupPos.x = groupPos.x - math.ceil(#copiedList / 20) * CARD_ROW_OFFSET - CARD_GROUP_OFFSET
spawnBag.spawn({
name = "evolvedWeaknesses",
cards = EVOLVED_WEAKNESSES,
globalPos = groupPos,
rotation = FACE_UP_ROTATION,
spread = true,
spreadCols = 20
})
end
function spawnRandomWeakness()
spawnBag.recall(true)
local allCardsBag = getObjectFromGUID(ALL_CARDS_BAG_GUID)
local weaknessId = allCardsBag.call("getRandomWeaknessId")
if (weaknessId == nil) then
broadcastToAll("All basic weaknesses are in play!", {0.9, 0.2, 0.2})
return
end
spawnBag.spawn({
name = "randomWeakness",
cards = { weaknessId },
globalPos = START_POSITIONS.randomWeakness,
rotation = FACE_UP_ROTATION,
})
end

View File

@ -1,3 +1,45 @@
BONDED_CARD_LIST = {
"05314", -- Soothing Melody
"06277", -- Wish Eater
"06019", -- Bloodlust
"06022", -- Pendant of the Queen
"05317", -- Blood-rite
"06113", -- Essence of the Dream
"06028", -- Stars Are Right
"06025", -- Guardian of the Crystallizer
"06283", -- Unbound Beast
"06032", -- Zeal
"06031", -- Hope
"06033", -- Augur
"06331", -- Dream Parasite
"06015a", -- Dream-Gate
}
UPGRADE_SHEET_LIST = {
"09040-c", -- Alchemical Distillation
"09023-c", -- Custom Modifications
"09059-c", -- Damning Testimony
"09041-c", -- Emperical Hypothesis
"09060-c", -- Friends in Low Places
"09101-c", -- Grizzled
"09061-c", -- Honed Instinct
"09021-c", -- Hunter's Armor
"09119-c", -- Hyperphysical Shotcaster
"09079-c", -- Living Ink
"09100-c", -- Makeshift Trap
"09099-c", -- Pocket Multi Tool
"09081-c", -- Power Word
"09022-c", -- Runic Axe
"09080-c", -- Summoned Servitor
"09042-c", -- Raven's Quill
}
EVOLVED_WEAKNESSES = {
"04039",
"04041",
"04042",
}
------------------ START INVESTIGATOR DATA DEFINITION ------------------
INVESTIGATOR_GROUPS = {
Guardian = {
@ -13,10 +55,6 @@ INVESTIGATOR_GROUPS = {
"D2",
"R3",
"D3",
"R4",
"D4",
"R5",
"D5",
},
}
@ -25,13 +63,13 @@ INVESTIGATORS["Roland Banks"] = {
cards = { "01001", "01001-promo", "01001-p", "01001-pf", "01001-pb", },
minicards = { "01001-m", "01001-promo-m", },
signatures = { "01006", "01007", "90030", "90031", },
starterDeck = "1462",
starterDeck = "2624931",
}
INVESTIGATORS["Daisy Walker"] = {
cards = { "01002", "01002-p", "01002-pf", "01002-pb", },
minicards = { "01002-m", },
signatures = { "01008", "01009", "90002", "90003" },
starterDeck = "42652",
starterDeck = "2624938",
}
------------------ END INVESTIGATOR DATA DEFINITION ------------------
INVESTIGATORS["R2"] = INVESTIGATORS["Roland Banks"]

View File

@ -663,7 +663,7 @@ function clickableClues(showCounter)
local pos = self.positionToWorld({x = -1.12, y = 0.05, z = 0.7})
for i = 1, clueCount do
pos.y = pos.y + 0.045 * i
TokenManager.spawnToken(pos, "clue", PLAY_ZONE_ROTATION)
tokenManager.spawnToken(pos, "clue", PLAY_ZONE_ROTATION)
end
end
end

View File

@ -3,26 +3,59 @@ do
local internal = { }
local MAT_IDS = {
White = "8b081b",
White = "8b081b",
Orange = "bd0ff4",
Green = "383d8b",
Red = "0840d5"
Green = "383d8b",
Red = "0840d5"
}
local CLUE_COUNTER_GUIDS = {
White = "37be78",
White = "37be78",
Orange = "1769ed",
Green = "032300",
Red = "d86b7c"
Green = "032300",
Red = "d86b7c"
}
local CLUE_CLICKER_GUIDS = {
White = "db85d6",
White = "db85d6",
Orange = "3f22e5",
Green = "891403",
Red = "4111de"
Green = "891403",
Red = "4111de"
}
-- Returns the color of the by position requested playermat as string
---@param startPos Table Position of the search, table get's roughly cut into 4 quarters to assign a playermat
PlaymatApi.getMatColorByPosition = function(startPos)
if startPos.x < -42 then
if startPos.z > 0 then
return "White"
else
return "Orange"
end
else
if startPos.z > 0 then
return "Green"
else
return "Red"
end
end
end
-- Returns the draw deck of the requested playmat
---@param matColor String Color of the playermat
PlaymatApi.getDrawDeck = function(matColor)
local mat = getObjectFromGUID(MAT_IDS[matColor])
mat.call("getDrawDiscardDecks")
return mat.getVar("drawDeck")
end
-- Returns the position of the discard pile of the requested playmat
---@param matColor String Color of the playermat
PlaymatApi.getDiscardPosition = function(matColor)
local mat = getObjectFromGUID(MAT_IDS[matColor])
return mat.getTable("DISCARD_PILE_POSITION")
end
-- Sets the requested playermat's snap points to limit snapping to matching card types or not. If
-- matchTypes is true, the main card slot snap points will only snap assets, while the
-- investigator area point will only snap Investigators. If matchTypes is false, snap points will
@ -79,7 +112,7 @@ do
-- Convenience function to look up a mat's object by color, or get all mats.
---@param matColor String for one of the active player colors - White, Orange, Green, Red. Also
-- accepts "All" as a special value which will return all four mats.
---@return Array of playermat objects. If a single mat is requested, will return a single-element
---@return: Array of playermat objects. If a single mat is requested, will return a single-element
-- array to simplify processing by consumers.
internal.getMatForColor = function(matColor)
local targetMatGuid = MAT_IDS[matColor]

View File

@ -104,4 +104,17 @@
</Panel>
</VerticalLayout>
<!-- Title Splash when starting a scenario -->
<Text id="title_splash"
fontSize="150"
font="font_teutonic-arkham"
outline="black"
outlineSize="3 -3"
showAnimation="FadeIn"
hideAnimation="FadeOut"
active="false"
animationDuration="2"
horizontalOverflow="Wrap">
</Text>
<Include src="OptionPanel.xml"/>

View File

@ -142,6 +142,20 @@
</Cell>
</Row>
<!-- Option: splash scenario name on setup -->
<Row class="option-text">
<Cell class="option-text">
<VerticalLayout class="text-column">
<Text class="option-header">Show Scenario Title on Setup</Text>
<Text class="description">Fade in the name of the scenario for 2 seconds when placing down a scenario.</Text>
</VerticalLayout>
</Cell>
<Cell class="option-button">
<Toggle id="showTitleSplash"
onValueChanged="onClick_toggleOption(showTitleSplash)"/>
</Cell>
</Row>
<!-- Group: fan-made accessories -->
<Row class="group-header">
<Cell class="group-header">