diff --git a/art/stages/hidden-garden/atmosphere.json b/art/stages/hidden-garden/atmosphere.json index 9283a30..303d44c 100644 --- a/art/stages/hidden-garden/atmosphere.json +++ b/art/stages/hidden-garden/atmosphere.json @@ -9,6 +9,7 @@ "03-rear": { "sway": 1.6, "reach": 300, "foliage": 0.3 }, "04-water": { "ripple": 1.5, "shimmer": 0.5 }, "05-play": { "sway": 1.5, "reach": 28, "anchor": "top", "foliage": 0.35 }, + "06-pillars": { "sway": 0 }, "06-near": { "sway": 2.4, "reach": 36, "anchor": "base", "foliage": 0.6, "bob": 0, "blur": 0 }, "07-foreground": { "sway": 3.5, "reach": 150, "foliage": 0.15, "bob": 1, "blur": 1.25 } }, @@ -16,6 +17,7 @@ "03-rear": { "sway": 0 }, "04-water": { "ripple": 0.8, "shimmer": 0.3 }, "05-play": { "sway": 0 }, + "06-pillars": { "sway": 0 }, "06-near": { "sway": 1.2, "reach": 80, "anchor": "base", "foliage": 0.6 } }, "leaves": { diff --git a/art/stages/hidden-garden/export.json b/art/stages/hidden-garden/export.json index fac4d93..9d4e2b2 100644 --- a/art/stages/hidden-garden/export.json +++ b/art/stages/hidden-garden/export.json @@ -90,6 +90,12 @@ "z": 5, "scroll": 1.0, "optional": true + }, + "06-pillars": { + "file": "layers/{name}.06-pillars.png", + "z": 6, + "scroll": 1.0, + "optional": true } }, "states": { @@ -100,6 +106,7 @@ "03-rear", "04-water", "05-play", + "06-pillars", "06-near", "07-foreground" ], @@ -111,6 +118,7 @@ "04-water", "05-play", "05-tears", + "06-pillars", "06-near", "07-foreground" ] diff --git a/art/stages/hidden-garden/hg-2-1.anomaly.collision.json b/art/stages/hidden-garden/hg-2-1.anomaly.collision.json index a8af158..0ea47e8 100644 --- a/art/stages/hidden-garden/hg-2-1.anomaly.collision.json +++ b/art/stages/hidden-garden/hg-2-1.anomaly.collision.json @@ -6,108 +6,6 @@ 720 ], "walk": [ - [ - [ - 0, - 340 - ], - [ - 48, - 340 - ], - [ - 96, - 340 - ], - [ - 144, - 340 - ], - [ - 192, - 340 - ], - [ - 240, - 340 - ], - [ - 288, - 340 - ], - [ - 336, - 340 - ], - [ - 384, - 340 - ], - [ - 432, - 340 - ], - [ - 480, - 340 - ], - [ - 528, - 340 - ], - [ - 576, - 340 - ], - [ - 624, - 340 - ], - [ - 672, - 340 - ], - [ - 720, - 340 - ], - [ - 768, - 340 - ], - [ - 816, - 340 - ], - [ - 864, - 340 - ], - [ - 912, - 340 - ], - [ - 960, - 340 - ], - [ - 1008, - 340 - ], - [ - 1056, - 340 - ], - [ - 1104, - 340 - ], - [ - 1136, - 340 - ] - ], [ [ 0, @@ -372,114 +270,54 @@ ], [ [ - 1204, - 341 - ], - [ - 1252, - 341 - ], - [ - 1300, - 341 - ], - [ - 1348, - 341 - ], - [ - 1396, - 341 - ], - [ - 1444, - 341 - ], - [ - 1492, - 341 - ], - [ - 1540, - 341 - ], - [ - 1588, - 341 - ], - [ - 1636, - 341 - ], - [ - 1684, - 341 - ], - [ - 1732, - 341 - ], - [ - 1780, - 341 - ], - [ - 1828, - 341 - ], - [ - 1876, - 341 - ], - [ - 1924, - 341 + 2036, + 629 ], [ - 1972, - 341 + 2052, + 630 ], [ - 2020, - 341 + 2076, + 631 ], [ - 2068, - 341 + 2084, + 631 ], [ - 2116, - 341 + 2132, + 631 ], [ 2160, - 341 + 631 ] ], [ [ - 2036, - 629 + 0, + 342 ], [ - 2052, - 630 + 720, + 342 ], [ - 2076, - 631 + 760, + 357 ], [ - 2084, - 631 + 1140, + 357 ], [ - 2132, - 631 + 1180, + 342 ], [ 2160, - 631 + 342 ] ] ], @@ -487,18 +325,18 @@ "oneWay": [], "climb": [ { - "x": 452, - "y": 161, - "w": 26, - "h": 522, + "x": 447, + "y": 342, + "w": 35, + "h": 261, "kind": "ladder", "id": "hgz-0-1-ladder-0" }, { - "x": 1821, - "y": 161, - "w": 25, - "h": 546, + "x": 1817, + "y": 342, + "w": 33, + "h": 264, "kind": "ladder", "id": "hgz-0-1-ladder-1" } @@ -516,9 +354,9 @@ "hazard": [], "interact": [ { - "x": 1095, - "y": 264, - "w": 80, + "x": 895, + "y": 281, + "w": 100, "h": 76, "kind": "door", "id": "cellar-stairs", @@ -526,8 +364,8 @@ "note": "Under the shed: the first archive note in a crate – an experiment log that mentions a 'mascot-shaped containment vessel'. Warm light: a place to breathe (secret horror: calm first)." }, { - "x": 950, - "y": 546, + "x": 200, + "y": 543, "w": 60, "h": 60, "kind": "note", @@ -536,8 +374,8 @@ "text": "Coherence holds 11 s. The pond shows the campus twice.\nIn the second copy the rovers are already awake.\nContainment vessel ordered: 1 x mascot-shaped, Campus Store, 1.5 ft. It fits." }, { - "x": 2050, - "y": 281, + "x": 1350, + "y": 282, "w": 60, "h": 60, "kind": "note", @@ -546,23 +384,83 @@ "text": "It waves.\nNobody taught it to wave." } ], - "spawn": [], + "spawn": [ + { + "id": "hg-2-1-lamp-0", + "kind": "lamp", + "x": 175, + "y": 265 + }, + { + "id": "hg-2-1-lamp-1", + "kind": "lamp", + "x": 855, + "y": 245 + }, + { + "id": "hg-2-1-lamp-2", + "kind": "lamp", + "x": 1335, + "y": 280 + }, + { + "id": "hg-2-1-lamp-3", + "kind": "lamp", + "x": 2005, + "y": 265 + }, + { + "id": "hg-2-1-lamp-4", + "kind": "lamp", + "x": 178, + "y": 515 + }, + { + "id": "hg-2-1-lamp-5", + "kind": "lamp", + "x": 850, + "y": 505 + }, + { + "id": "hg-2-1-lamp-6", + "kind": "lamp", + "x": 1335, + "y": 525 + }, + { + "id": "hg-2-1-lamp-7", + "kind": "lamp", + "x": 1995, + "y": 515 + }, + { + "id": "hg-2-1-drip-0", + "kind": "drip", + "x": 300, + "y": 104 + }, + { + "id": "hg-2-1-drip-1", + "kind": "drip", + "x": 760, + "y": 104 + }, + { + "id": "hg-2-1-drip-2", + "kind": "drip", + "x": 1460, + "y": 104 + }, + { + "id": "hg-2-1-drip-3", + "kind": "drip", + "x": 1900, + "y": 104 + } + ], "ledge": [], "glideLift": [], "terrain": [ - { - "x": [ - 0, - 1136 - ], - "y": [ - 328, - 352 - ], - "kind": "ground", - "material": "stone", - "note": "the floor" - }, { "x": [ 0, @@ -578,12 +476,12 @@ }, { "x": [ - 1204, + 2036, 2160 ], "y": [ - 329, - 353 + 617, + 643 ], "kind": "ground", "material": "stone", @@ -591,12 +489,12 @@ }, { "x": [ - 2036, + 0, 2160 ], "y": [ - 617, - 643 + 330, + 369 ], "kind": "ground", "material": "stone", diff --git a/art/stages/hidden-garden/hg-2-1.clean.collision.json b/art/stages/hidden-garden/hg-2-1.clean.collision.json index acb0474..e104ea9 100644 --- a/art/stages/hidden-garden/hg-2-1.clean.collision.json +++ b/art/stages/hidden-garden/hg-2-1.clean.collision.json @@ -6,108 +6,6 @@ 720 ], "walk": [ - [ - [ - 0, - 340 - ], - [ - 48, - 340 - ], - [ - 96, - 340 - ], - [ - 144, - 340 - ], - [ - 192, - 340 - ], - [ - 240, - 340 - ], - [ - 288, - 340 - ], - [ - 336, - 340 - ], - [ - 384, - 340 - ], - [ - 432, - 340 - ], - [ - 480, - 340 - ], - [ - 528, - 340 - ], - [ - 576, - 340 - ], - [ - 624, - 340 - ], - [ - 672, - 340 - ], - [ - 720, - 340 - ], - [ - 768, - 340 - ], - [ - 816, - 340 - ], - [ - 864, - 340 - ], - [ - 912, - 340 - ], - [ - 960, - 340 - ], - [ - 1008, - 340 - ], - [ - 1056, - 340 - ], - [ - 1104, - 340 - ], - [ - 1136, - 340 - ] - ], [ [ 0, @@ -372,114 +270,54 @@ ], [ [ - 1204, - 341 - ], - [ - 1252, - 341 - ], - [ - 1300, - 341 - ], - [ - 1348, - 341 - ], - [ - 1396, - 341 - ], - [ - 1444, - 341 - ], - [ - 1492, - 341 - ], - [ - 1540, - 341 - ], - [ - 1588, - 341 - ], - [ - 1636, - 341 - ], - [ - 1684, - 341 - ], - [ - 1732, - 341 - ], - [ - 1780, - 341 - ], - [ - 1828, - 341 - ], - [ - 1876, - 341 - ], - [ - 1924, - 341 + 2036, + 629 ], [ - 1972, - 341 + 2052, + 630 ], [ - 2020, - 341 + 2076, + 631 ], [ - 2068, - 341 + 2084, + 631 ], [ - 2116, - 341 + 2132, + 631 ], [ 2160, - 341 + 631 ] ], [ [ - 2036, - 629 + 0, + 342 ], [ - 2052, - 630 + 720, + 342 ], [ - 2076, - 631 + 760, + 357 ], [ - 2084, - 631 + 1140, + 357 ], [ - 2132, - 631 + 1180, + 342 ], [ 2160, - 631 + 342 ] ] ], @@ -487,18 +325,18 @@ "oneWay": [], "climb": [ { - "x": 452, - "y": 161, - "w": 26, - "h": 522, + "x": 447, + "y": 342, + "w": 35, + "h": 261, "kind": "ladder", "id": "hgz-0-1-ladder-0" }, { - "x": 1821, - "y": 161, - "w": 25, - "h": 546, + "x": 1817, + "y": 342, + "w": 33, + "h": 264, "kind": "ladder", "id": "hgz-0-1-ladder-1" } @@ -516,9 +354,9 @@ "hazard": [], "interact": [ { - "x": 1095, - "y": 264, - "w": 80, + "x": 895, + "y": 281, + "w": 100, "h": 76, "kind": "door", "id": "cellar-stairs", @@ -526,8 +364,8 @@ "note": "Under the shed: the first archive note in a crate – an experiment log that mentions a 'mascot-shaped containment vessel'. Warm light: a place to breathe (secret horror: calm first)." }, { - "x": 950, - "y": 546, + "x": 200, + "y": 543, "w": 60, "h": 60, "kind": "note", @@ -536,8 +374,8 @@ "text": "Coherence holds 11 s. The pond shows the campus twice.\nIn the second copy the rovers are already awake.\nContainment vessel ordered: 1 x mascot-shaped, Campus Store, 1.5 ft. It fits." }, { - "x": 2050, - "y": 281, + "x": 1350, + "y": 282, "w": 60, "h": 60, "kind": "note", @@ -551,27 +389,86 @@ "id": "dexter-cellar", "kind": "dexter-sighting", "x": 1500, - "y": 341, + "y": 342, "act": "beckon", "face": -1 + }, + { + "id": "hg-2-1-lamp-0", + "kind": "lamp", + "x": 175, + "y": 265 + }, + { + "id": "hg-2-1-lamp-1", + "kind": "lamp", + "x": 855, + "y": 245 + }, + { + "id": "hg-2-1-lamp-2", + "kind": "lamp", + "x": 1335, + "y": 280 + }, + { + "id": "hg-2-1-lamp-3", + "kind": "lamp", + "x": 2005, + "y": 265 + }, + { + "id": "hg-2-1-lamp-4", + "kind": "lamp", + "x": 178, + "y": 515 + }, + { + "id": "hg-2-1-lamp-5", + "kind": "lamp", + "x": 850, + "y": 505 + }, + { + "id": "hg-2-1-lamp-6", + "kind": "lamp", + "x": 1335, + "y": 525 + }, + { + "id": "hg-2-1-lamp-7", + "kind": "lamp", + "x": 1995, + "y": 515 + }, + { + "id": "hg-2-1-drip-0", + "kind": "drip", + "x": 300, + "y": 104 + }, + { + "id": "hg-2-1-drip-1", + "kind": "drip", + "x": 760, + "y": 104 + }, + { + "id": "hg-2-1-drip-2", + "kind": "drip", + "x": 1460, + "y": 104 + }, + { + "id": "hg-2-1-drip-3", + "kind": "drip", + "x": 1900, + "y": 104 } ], "ledge": [], "glideLift": [], "terrain": [ - { - "x": [ - 0, - 1136 - ], - "y": [ - 328, - 352 - ], - "kind": "ground", - "material": "stone", - "note": "the floor" - }, { "x": [ 0, @@ -587,12 +484,12 @@ }, { "x": [ - 1204, + 2036, 2160 ], "y": [ - 329, - 353 + 617, + 643 ], "kind": "ground", "material": "stone", @@ -600,12 +497,12 @@ }, { "x": [ - 2036, + 0, 2160 ], "y": [ - 617, - 643 + 330, + 369 ], "kind": "ground", "material": "stone", diff --git a/art/stages/hidden-garden/hg-3-1.anomaly.collision.json b/art/stages/hidden-garden/hg-3-1.anomaly.collision.json index 2ab1c66..cc75803 100644 --- a/art/stages/hidden-garden/hg-3-1.anomaly.collision.json +++ b/art/stages/hidden-garden/hg-3-1.anomaly.collision.json @@ -6,128 +6,6 @@ 720 ], "walk": [ - [ - [ - 0, - 138 - ], - [ - 48, - 138 - ], - [ - 96, - 138 - ], - [ - 144, - 138 - ], - [ - 192, - 138 - ], - [ - 240, - 138 - ], - [ - 288, - 138 - ], - [ - 336, - 138 - ], - [ - 384, - 138 - ], - [ - 432, - 138 - ], - [ - 480, - 138 - ], - [ - 528, - 138 - ], - [ - 576, - 138 - ], - [ - 624, - 138 - ], - [ - 672, - 138 - ], - [ - 720, - 138 - ], - [ - 768, - 138 - ], - [ - 816, - 138 - ], - [ - 864, - 138 - ], - [ - 912, - 138 - ], - [ - 960, - 138 - ], - [ - 1008, - 138 - ], - [ - 1056, - 138 - ], - [ - 1104, - 138 - ], - [ - 1152, - 138 - ], - [ - 1200, - 138 - ], - [ - 1248, - 138 - ], - [ - 1296, - 138 - ], - [ - 1344, - 138 - ], - [ - 1360, - 138 - ] - ], [ [ 0, @@ -626,40 +504,6 @@ 659 ] ], - [ - [ - 1360, - 140 - ], - [ - 1408, - 140 - ], - [ - 1456, - 140 - ], - [ - 1504, - 140 - ], - [ - 1552, - 140 - ], - [ - 1600, - 140 - ], - [ - 1648, - 140 - ], - [ - 1664, - 140 - ] - ], [ [ 1824, @@ -776,44 +620,6 @@ 628 ] ], - [ - [ - 1664, - 140 - ], - [ - 1712, - 140 - ], - [ - 1760, - 140 - ], - [ - 1808, - 140 - ], - [ - 1856, - 140 - ], - [ - 1904, - 140 - ], - [ - 1952, - 140 - ], - [ - 2000, - 140 - ], - [ - 2016, - 140 - ] - ], [ [ 1836, @@ -842,11 +648,19 @@ "climb": [ { "x": 1535, - "y": 84, + "y": 349, "w": 52, - "h": 588, + "h": 278, "kind": "ladder", "id": "hgz-1-1-ladder-0" + }, + { + "x": 358, + "y": 342, + "w": 38, + "h": 300, + "kind": "ladder", + "id": "hgz-1-1-ladder-m0" } ], "solid": [], @@ -865,7 +679,7 @@ }, { "x": 730, - "y": 78, + "y": 289, "w": 60, "h": 60, "kind": "note", @@ -904,24 +718,65 @@ "kind": "paper-boat", "x": 960, "y": 650 + }, + { + "id": "hg-3-1-lamp-0", + "kind": "lamp", + "x": 640, + "y": 235 + }, + { + "id": "hg-3-1-lamp-1", + "kind": "lamp", + "x": 1170, + "y": 235 + }, + { + "id": "hg-3-1-lamp-2", + "kind": "lamp", + "x": 130, + "y": 560 + }, + { + "id": "hg-3-1-lamp-3", + "kind": "lamp", + "x": 635, + "y": 560 + }, + { + "id": "hg-3-1-lamp-4", + "kind": "lamp", + "x": 1160, + "y": 560 + }, + { + "id": "hg-3-1-drip-0", + "kind": "drip", + "x": 260, + "y": 104 + }, + { + "id": "hg-3-1-drip-1", + "kind": "drip", + "x": 900, + "y": 104 + }, + { + "id": "hg-3-1-drip-2", + "kind": "drip", + "x": 1380, + "y": 104 + }, + { + "id": "hg-3-1-drip-3", + "kind": "drip", + "x": 1850, + "y": 104 } ], "ledge": [], "glideLift": [], "terrain": [ - { - "x": [ - 0, - 1360 - ], - "y": [ - 126, - 150 - ], - "kind": "ground", - "material": "stone", - "note": "the floor" - }, { "x": [ 0, @@ -1013,19 +868,6 @@ "material": "stone", "note": "the floor" }, - { - "x": [ - 1360, - 1664 - ], - "y": [ - 128, - 152 - ], - "kind": "ground", - "material": "stone", - "note": "the floor" - }, { "x": [ 1824, @@ -1052,19 +894,6 @@ "material": "stone", "note": "the floor" }, - { - "x": [ - 1664, - 2016 - ], - "y": [ - 128, - 152 - ], - "kind": "ground", - "material": "stone", - "note": "the floor" - }, { "x": [ 1836, diff --git a/art/stages/hidden-garden/hg-3-1.clean.collision.json b/art/stages/hidden-garden/hg-3-1.clean.collision.json index bd08112..c864a8c 100644 --- a/art/stages/hidden-garden/hg-3-1.clean.collision.json +++ b/art/stages/hidden-garden/hg-3-1.clean.collision.json @@ -6,128 +6,6 @@ 720 ], "walk": [ - [ - [ - 0, - 138 - ], - [ - 48, - 138 - ], - [ - 96, - 138 - ], - [ - 144, - 138 - ], - [ - 192, - 138 - ], - [ - 240, - 138 - ], - [ - 288, - 138 - ], - [ - 336, - 138 - ], - [ - 384, - 138 - ], - [ - 432, - 138 - ], - [ - 480, - 138 - ], - [ - 528, - 138 - ], - [ - 576, - 138 - ], - [ - 624, - 138 - ], - [ - 672, - 138 - ], - [ - 720, - 138 - ], - [ - 768, - 138 - ], - [ - 816, - 138 - ], - [ - 864, - 138 - ], - [ - 912, - 138 - ], - [ - 960, - 138 - ], - [ - 1008, - 138 - ], - [ - 1056, - 138 - ], - [ - 1104, - 138 - ], - [ - 1152, - 138 - ], - [ - 1200, - 138 - ], - [ - 1248, - 138 - ], - [ - 1296, - 138 - ], - [ - 1344, - 138 - ], - [ - 1360, - 138 - ] - ], [ [ 0, @@ -626,40 +504,6 @@ 659 ] ], - [ - [ - 1360, - 140 - ], - [ - 1408, - 140 - ], - [ - 1456, - 140 - ], - [ - 1504, - 140 - ], - [ - 1552, - 140 - ], - [ - 1600, - 140 - ], - [ - 1648, - 140 - ], - [ - 1664, - 140 - ] - ], [ [ 1824, @@ -776,44 +620,6 @@ 628 ] ], - [ - [ - 1664, - 140 - ], - [ - 1712, - 140 - ], - [ - 1760, - 140 - ], - [ - 1808, - 140 - ], - [ - 1856, - 140 - ], - [ - 1904, - 140 - ], - [ - 1952, - 140 - ], - [ - 2000, - 140 - ], - [ - 2016, - 140 - ] - ], [ [ 1836, @@ -842,11 +648,19 @@ "climb": [ { "x": 1535, - "y": 84, + "y": 349, "w": 52, - "h": 588, + "h": 278, "kind": "ladder", "id": "hgz-1-1-ladder-0" + }, + { + "x": 358, + "y": 342, + "w": 38, + "h": 300, + "kind": "ladder", + "id": "hgz-1-1-ladder-m0" } ], "solid": [], @@ -865,7 +679,7 @@ }, { "x": 730, - "y": 78, + "y": 289, "w": 60, "h": 60, "kind": "note", @@ -912,24 +726,65 @@ "y": 646, "act": "creepy", "face": -1 + }, + { + "id": "hg-3-1-lamp-0", + "kind": "lamp", + "x": 640, + "y": 235 + }, + { + "id": "hg-3-1-lamp-1", + "kind": "lamp", + "x": 1170, + "y": 235 + }, + { + "id": "hg-3-1-lamp-2", + "kind": "lamp", + "x": 130, + "y": 560 + }, + { + "id": "hg-3-1-lamp-3", + "kind": "lamp", + "x": 635, + "y": 560 + }, + { + "id": "hg-3-1-lamp-4", + "kind": "lamp", + "x": 1160, + "y": 560 + }, + { + "id": "hg-3-1-drip-0", + "kind": "drip", + "x": 260, + "y": 104 + }, + { + "id": "hg-3-1-drip-1", + "kind": "drip", + "x": 900, + "y": 104 + }, + { + "id": "hg-3-1-drip-2", + "kind": "drip", + "x": 1380, + "y": 104 + }, + { + "id": "hg-3-1-drip-3", + "kind": "drip", + "x": 1850, + "y": 104 } ], "ledge": [], "glideLift": [], "terrain": [ - { - "x": [ - 0, - 1360 - ], - "y": [ - 126, - 150 - ], - "kind": "ground", - "material": "stone", - "note": "the floor" - }, { "x": [ 0, @@ -1021,19 +876,6 @@ "material": "stone", "note": "the floor" }, - { - "x": [ - 1360, - 1664 - ], - "y": [ - 128, - 152 - ], - "kind": "ground", - "material": "stone", - "note": "the floor" - }, { "x": [ 1824, @@ -1060,19 +902,6 @@ "material": "stone", "note": "the floor" }, - { - "x": [ - 1664, - 2016 - ], - "y": [ - 128, - 152 - ], - "kind": "ground", - "material": "stone", - "note": "the floor" - }, { "x": [ 1836, diff --git a/art/stages/hidden-garden/hg-4-1.anomaly.collision.json b/art/stages/hidden-garden/hg-4-1.anomaly.collision.json index be0a445..d5bb7e4 100644 --- a/art/stages/hidden-garden/hg-4-1.anomaly.collision.json +++ b/art/stages/hidden-garden/hg-4-1.anomaly.collision.json @@ -642,7 +642,38 @@ "text": "Down here everything is the right way round.\nIt is the garden that hangs upside down.\nDo not feed the vessel." } ], - "spawn": [], + "spawn": [ + { + "id": "hg-4-1-drip-0", + "kind": "drip", + "x": 330, + "y": 162 + }, + { + "id": "hg-4-1-drip-1", + "kind": "drip", + "x": 650, + "y": 312 + }, + { + "id": "hg-4-1-drip-2", + "kind": "drip", + "x": 1010, + "y": 414 + }, + { + "id": "hg-4-1-drip-3", + "kind": "drip", + "x": 1590, + "y": 446 + }, + { + "id": "hg-4-1-drip-4", + "kind": "drip", + "x": 1720, + "y": 262 + } + ], "ledge": [], "glideLift": [], "terrain": [ diff --git a/art/stages/hidden-garden/hg-4-1.clean.collision.json b/art/stages/hidden-garden/hg-4-1.clean.collision.json index 0c85b8a..654567d 100644 --- a/art/stages/hidden-garden/hg-4-1.clean.collision.json +++ b/art/stages/hidden-garden/hg-4-1.clean.collision.json @@ -646,10 +646,40 @@ { "id": "dexter-reflection", "kind": "dexter-sighting", - "x": 1340, - "y": 115, - "act": "wait", + "x": 760, + "y": 643, + "act": "beckon", "face": -1 + }, + { + "id": "hg-4-1-drip-0", + "kind": "drip", + "x": 330, + "y": 162 + }, + { + "id": "hg-4-1-drip-1", + "kind": "drip", + "x": 650, + "y": 312 + }, + { + "id": "hg-4-1-drip-2", + "kind": "drip", + "x": 1010, + "y": 414 + }, + { + "id": "hg-4-1-drip-3", + "kind": "drip", + "x": 1590, + "y": 446 + }, + { + "id": "hg-4-1-drip-4", + "kind": "drip", + "x": 1720, + "y": 262 } ], "ledge": [], diff --git a/art/stages/hidden-garden/layers.json b/art/stages/hidden-garden/layers.json index bb86292..a59b31c 100644 --- a/art/stages/hidden-garden/layers.json +++ b/art/stages/hidden-garden/layers.json @@ -303,6 +303,10 @@ { "layer": "06-near", "alpha": "masks/hg-2-1.06-near.png" + }, + { + "layer": "06-pillars", + "alpha": "masks/hg-2-1.06-pillars.png" } ] }, diff --git a/art/stages/hidden-garden/layers/hg-2-1.03-rear.png b/art/stages/hidden-garden/layers/hg-2-1.03-rear.png index ece35c8..9e052bf 100644 Binary files a/art/stages/hidden-garden/layers/hg-2-1.03-rear.png and b/art/stages/hidden-garden/layers/hg-2-1.03-rear.png differ diff --git a/art/stages/hidden-garden/layers/hg-2-1.04-water.png b/art/stages/hidden-garden/layers/hg-2-1.04-water.png index a599bb5..4713abc 100644 Binary files a/art/stages/hidden-garden/layers/hg-2-1.04-water.png and b/art/stages/hidden-garden/layers/hg-2-1.04-water.png differ diff --git a/art/stages/hidden-garden/layers/hg-2-1.05-play.png b/art/stages/hidden-garden/layers/hg-2-1.05-play.png index a6ead5a..b2766bd 100644 Binary files a/art/stages/hidden-garden/layers/hg-2-1.05-play.png and b/art/stages/hidden-garden/layers/hg-2-1.05-play.png differ diff --git a/art/stages/hidden-garden/layers/hg-2-1.06-pillars.png b/art/stages/hidden-garden/layers/hg-2-1.06-pillars.png new file mode 100644 index 0000000..8d17010 Binary files /dev/null and b/art/stages/hidden-garden/layers/hg-2-1.06-pillars.png differ diff --git a/art/stages/hidden-garden/masks/hg-2-1.06-pillars.png b/art/stages/hidden-garden/masks/hg-2-1.06-pillars.png new file mode 100644 index 0000000..2efa464 Binary files /dev/null and b/art/stages/hidden-garden/masks/hg-2-1.06-pillars.png differ diff --git a/art/stages/hidden-garden/surfaces.json b/art/stages/hidden-garden/surfaces.json index bf3239c..9d56824 100644 --- a/art/stages/hidden-garden/surfaces.json +++ b/art/stages/hidden-garden/surfaces.json @@ -54,13 +54,11 @@ {"x": [1020, 1330], "material": "wood", "note": "plank: pier"} ], "hg-2-1": [ - {"x": [0, 1136], "y": [328, 352], "material": "stone", "note": "ground: the floor"}, {"x": [0, 2036], "y": [589, 619], "material": "stone", "note": "ground: the floor"}, - {"x": [1204, 2160], "y": [329, 353], "material": "stone", "note": "ground: the floor"}, - {"x": [2036, 2160], "y": [617, 643], "material": "stone", "note": "ground: the floor"} + {"x": [2036, 2160], "y": [617, 643], "material": "stone", "note": "ground: the floor"}, + {"x": [0, 2160], "y": [330, 369], "material": "stone", "note": "ground: the floor"} ], "hg-3-1": [ - {"x": [0, 1360], "y": [126, 150], "material": "stone", "note": "ground: the floor"}, {"x": [0, 568], "y": [330, 354], "material": "stone", "note": "ground: the floor"}, {"x": [0, 140], "y": [625, 652], "material": "stone", "note": "ground: the floor"}, {"x": [140, 344], "y": [630, 663], "material": "stone", "note": "ground: the floor"}, @@ -68,10 +66,8 @@ {"x": [564, 1236], "y": [631, 664], "material": "stone", "note": "ground: the floor"}, {"x": [568, 1744], "y": [337, 361], "material": "stone", "note": "ground: the floor"}, {"x": [1236, 1492], "y": [626, 671], "material": "stone", "note": "ground: the floor"}, - {"x": [1360, 1664], "y": [128, 152], "material": "stone", "note": "ground: the floor"}, {"x": [1824, 2160], "y": [630, 661], "material": "stone", "note": "ground: the floor"}, {"x": [1492, 1824], "y": [613, 640], "material": "stone", "note": "ground: the floor"}, - {"x": [1664, 2016], "y": [128, 152], "material": "stone", "note": "ground: the floor"}, {"x": [1836, 2004], "y": [340, 364], "material": "stone", "note": "ground: the floor"} ], "hg-4-1": [ diff --git a/art/zones/hidden-garden/collision/hgz-0-1.collision.json b/art/zones/hidden-garden/collision/hgz-0-1.collision.json index 0753f99..7632c86 100644 --- a/art/zones/hidden-garden/collision/hgz-0-1.collision.json +++ b/art/zones/hidden-garden/collision/hgz-0-1.collision.json @@ -6,108 +6,6 @@ 720 ], "walk": [ - [ - [ - 0, - 340 - ], - [ - 48, - 340 - ], - [ - 96, - 340 - ], - [ - 144, - 340 - ], - [ - 192, - 340 - ], - [ - 240, - 340 - ], - [ - 288, - 340 - ], - [ - 336, - 340 - ], - [ - 384, - 340 - ], - [ - 432, - 340 - ], - [ - 480, - 340 - ], - [ - 528, - 340 - ], - [ - 576, - 340 - ], - [ - 624, - 340 - ], - [ - 672, - 340 - ], - [ - 720, - 340 - ], - [ - 768, - 340 - ], - [ - 816, - 340 - ], - [ - 864, - 340 - ], - [ - 912, - 340 - ], - [ - 960, - 340 - ], - [ - 1008, - 340 - ], - [ - 1056, - 340 - ], - [ - 1104, - 340 - ], - [ - 1136, - 340 - ] - ], [ [ 0, @@ -372,114 +270,54 @@ ], [ [ - 1204, - 341 - ], - [ - 1252, - 341 - ], - [ - 1300, - 341 - ], - [ - 1348, - 341 - ], - [ - 1396, - 341 - ], - [ - 1444, - 341 - ], - [ - 1492, - 341 - ], - [ - 1540, - 341 - ], - [ - 1588, - 341 - ], - [ - 1636, - 341 - ], - [ - 1684, - 341 - ], - [ - 1732, - 341 - ], - [ - 1780, - 341 - ], - [ - 1828, - 341 - ], - [ - 1876, - 341 - ], - [ - 1924, - 341 + 2036, + 629 ], [ - 1972, - 341 + 2052, + 630 ], [ - 2020, - 341 + 2076, + 631 ], [ - 2068, - 341 + 2084, + 631 ], [ - 2116, - 341 + 2132, + 631 ], [ 2160, - 341 + 631 ] ], [ [ - 2036, - 629 + 0, + 342 ], [ - 2052, - 630 + 720, + 342 ], [ - 2076, - 631 + 760, + 357 ], [ - 2084, - 631 + 1140, + 357 ], [ - 2132, - 631 + 1180, + 342 ], [ 2160, - 631 + 342 ] ] ], @@ -487,18 +325,18 @@ "oneWay": [], "climb": [ { - "x": 452, - "y": 161, - "w": 26, - "h": 522, + "x": 447, + "y": 342, + "w": 35, + "h": 261, "kind": "ladder", "id": "hgz-0-1-ladder-0" }, { - "x": 1821, - "y": 161, - "w": 25, - "h": 546, + "x": 1817, + "y": 342, + "w": 33, + "h": 264, "kind": "ladder", "id": "hgz-0-1-ladder-1" } @@ -516,9 +354,9 @@ "hazard": [], "interact": [ { - "x": 1095, - "y": 264, - "w": 80, + "x": 895, + "y": 281, + "w": 100, "h": 76, "kind": "door", "id": "cellar-stairs", @@ -526,8 +364,8 @@ "note": "Under the shed: the first archive note in a crate \u2013 an experiment log that mentions a 'mascot-shaped containment vessel'. Warm light: a place to breathe (secret horror: calm first)." }, { - "x": 950, - "y": 546, + "x": 200, + "y": 543, "w": 60, "h": 60, "kind": "note", @@ -536,8 +374,8 @@ "text": "Coherence holds 11 s. The pond shows the campus twice.\nIn the second copy the rovers are already awake.\nContainment vessel ordered: 1 x mascot-shaped, Campus Store, 1.5 ft. It fits." }, { - "x": 2050, - "y": 281, + "x": 1350, + "y": 282, "w": 60, "h": 60, "kind": "note", @@ -551,9 +389,81 @@ "id": "dexter-cellar", "kind": "dexter-sighting", "x": 1500, - "y": 341, + "y": 342, "act": "beckon", "face": -1 + }, + { + "id": "hgz-0-1-lamp-0", + "kind": "lamp", + "x": 175, + "y": 265 + }, + { + "id": "hgz-0-1-lamp-1", + "kind": "lamp", + "x": 855, + "y": 245 + }, + { + "id": "hgz-0-1-lamp-2", + "kind": "lamp", + "x": 1335, + "y": 280 + }, + { + "id": "hgz-0-1-lamp-3", + "kind": "lamp", + "x": 2005, + "y": 265 + }, + { + "id": "hgz-0-1-lamp-4", + "kind": "lamp", + "x": 178, + "y": 515 + }, + { + "id": "hgz-0-1-lamp-5", + "kind": "lamp", + "x": 850, + "y": 505 + }, + { + "id": "hgz-0-1-lamp-6", + "kind": "lamp", + "x": 1335, + "y": 525 + }, + { + "id": "hgz-0-1-lamp-7", + "kind": "lamp", + "x": 1995, + "y": 515 + }, + { + "id": "hgz-0-1-drip-0", + "kind": "drip", + "x": 300, + "y": 104 + }, + { + "id": "hgz-0-1-drip-1", + "kind": "drip", + "x": 760, + "y": 104 + }, + { + "id": "hgz-0-1-drip-2", + "kind": "drip", + "x": 1460, + "y": 104 + }, + { + "id": "hgz-0-1-drip-3", + "kind": "drip", + "x": 1900, + "y": 104 } ] } diff --git a/art/zones/hidden-garden/collision/hgz-1-1.collision.json b/art/zones/hidden-garden/collision/hgz-1-1.collision.json index 4712ed3..90aaf5d 100644 --- a/art/zones/hidden-garden/collision/hgz-1-1.collision.json +++ b/art/zones/hidden-garden/collision/hgz-1-1.collision.json @@ -6,128 +6,6 @@ 720 ], "walk": [ - [ - [ - 0, - 138 - ], - [ - 48, - 138 - ], - [ - 96, - 138 - ], - [ - 144, - 138 - ], - [ - 192, - 138 - ], - [ - 240, - 138 - ], - [ - 288, - 138 - ], - [ - 336, - 138 - ], - [ - 384, - 138 - ], - [ - 432, - 138 - ], - [ - 480, - 138 - ], - [ - 528, - 138 - ], - [ - 576, - 138 - ], - [ - 624, - 138 - ], - [ - 672, - 138 - ], - [ - 720, - 138 - ], - [ - 768, - 138 - ], - [ - 816, - 138 - ], - [ - 864, - 138 - ], - [ - 912, - 138 - ], - [ - 960, - 138 - ], - [ - 1008, - 138 - ], - [ - 1056, - 138 - ], - [ - 1104, - 138 - ], - [ - 1152, - 138 - ], - [ - 1200, - 138 - ], - [ - 1248, - 138 - ], - [ - 1296, - 138 - ], - [ - 1344, - 138 - ], - [ - 1360, - 138 - ] - ], [ [ 0, @@ -626,40 +504,6 @@ 659 ] ], - [ - [ - 1360, - 140 - ], - [ - 1408, - 140 - ], - [ - 1456, - 140 - ], - [ - 1504, - 140 - ], - [ - 1552, - 140 - ], - [ - 1600, - 140 - ], - [ - 1648, - 140 - ], - [ - 1664, - 140 - ] - ], [ [ 1824, @@ -776,44 +620,6 @@ 628 ] ], - [ - [ - 1664, - 140 - ], - [ - 1712, - 140 - ], - [ - 1760, - 140 - ], - [ - 1808, - 140 - ], - [ - 1856, - 140 - ], - [ - 1904, - 140 - ], - [ - 1952, - 140 - ], - [ - 2000, - 140 - ], - [ - 2016, - 140 - ] - ], [ [ 1836, @@ -842,11 +648,19 @@ "climb": [ { "x": 1535, - "y": 84, + "y": 349, "w": 52, - "h": 588, + "h": 278, "kind": "ladder", "id": "hgz-1-1-ladder-0" + }, + { + "x": 358, + "y": 342, + "w": 38, + "h": 300, + "kind": "ladder", + "id": "hgz-1-1-ladder-m0" } ], "solid": [], @@ -865,7 +679,7 @@ }, { "x": 730, - "y": 78, + "y": 289, "w": 60, "h": 60, "kind": "note", @@ -912,6 +726,60 @@ "y": 646, "act": "creepy", "face": -1 + }, + { + "id": "hgz-1-1-lamp-0", + "kind": "lamp", + "x": 640, + "y": 235 + }, + { + "id": "hgz-1-1-lamp-1", + "kind": "lamp", + "x": 1170, + "y": 235 + }, + { + "id": "hgz-1-1-lamp-2", + "kind": "lamp", + "x": 130, + "y": 560 + }, + { + "id": "hgz-1-1-lamp-3", + "kind": "lamp", + "x": 635, + "y": 560 + }, + { + "id": "hgz-1-1-lamp-4", + "kind": "lamp", + "x": 1160, + "y": 560 + }, + { + "id": "hgz-1-1-drip-0", + "kind": "drip", + "x": 260, + "y": 104 + }, + { + "id": "hgz-1-1-drip-1", + "kind": "drip", + "x": 900, + "y": 104 + }, + { + "id": "hgz-1-1-drip-2", + "kind": "drip", + "x": 1380, + "y": 104 + }, + { + "id": "hgz-1-1-drip-3", + "kind": "drip", + "x": 1850, + "y": 104 } ] } diff --git a/art/zones/hidden-garden/collision/hgz-2-1.collision.json b/art/zones/hidden-garden/collision/hgz-2-1.collision.json index 222b153..2931751 100644 --- a/art/zones/hidden-garden/collision/hgz-2-1.collision.json +++ b/art/zones/hidden-garden/collision/hgz-2-1.collision.json @@ -646,10 +646,40 @@ { "id": "dexter-reflection", "kind": "dexter-sighting", - "x": 1340, - "y": 115, - "act": "wait", + "x": 760, + "y": 643, + "act": "beckon", "face": -1 + }, + { + "id": "hgz-2-1-drip-0", + "kind": "drip", + "x": 330, + "y": 162 + }, + { + "id": "hgz-2-1-drip-1", + "kind": "drip", + "x": 650, + "y": 312 + }, + { + "id": "hgz-2-1-drip-2", + "kind": "drip", + "x": 1010, + "y": 414 + }, + { + "id": "hgz-2-1-drip-3", + "kind": "drip", + "x": 1590, + "y": 446 + }, + { + "id": "hgz-2-1-drip-4", + "kind": "drip", + "x": 1720, + "y": 262 } ] } diff --git a/docs/09-ART-PIPELINE.md b/docs/09-ART-PIPELINE.md index 170a65d..3d8c0b7 100644 --- a/docs/09-ART-PIPELINE.md +++ b/docs/09-ART-PIPELINE.md @@ -100,6 +100,14 @@ What the proof taught (rules now): - a room without sky takes `atmosphere.json` `indoor`: walls and floors stay still, weeds sway, water ripples gently; - `whole` paints again when its painting has a flat-black void (`black_void`, over 600 px). Checked layer by layer in debug mode, hg-3-1's back wall had a made-up black pipe mouth behind a girder (25 Oct). - **Rusty Creek goes through the same pipeline (#157):** `art/zones/rusty-creek/zone.json` from the stacked direction (#227), and `world_map.py`'s 3×3 map and blockout (seed 157). A zone names its own places in `zone.json` `"words"`: the creek has culverts and service rooms, never caves. First paint take (25 Oct, 9 calls): every chunk came back as the style master. The style master *is* Rusty Creek, so a "look only" reference that shows the very place being painted gets copied, guide and all. A zone's look reference must never be a picture of that zone; for the creek, either grow from its own approved plates (the Hidden Garden method) or paint with a reference of another place. +- **Played, the underground has to read (Alexander's playtest, 25 Oct):** the cellar's door was a purple box beside its painted door, a ladder ran up into the ceiling girder, the upper floor had a gap under a pillar, the hero walked in front of the steel pillars and off the row's open end, the prologue's guide followed him down and walked in the air, and nothing moved. The fixes are rules now: + - **What the trace gets wrong is measured** (`hidden_garden_proof.py` `MEASURED`): a floor that runs behind pillars is one floor; a ladder runs between the floors it joins, never past them; a door's box is on its painted door (a door pair is joined by id, not by x). + - **Columns the hero passes are in front of him:** `06-pillars` (z 6 like `06-near`, but still – the near strip sways), a measured mask per chunk (`PILLARS`), drawn over the actors. + - **A row's open end is a wall:** the game closes every row where a chunk has no neighbour (`collision.ts` `edgeWalls`). + - **The guide walks his own row** (`prologueGuide.ts` `guideLines`); the rooms below have their own sightings. + - **What can be used shows itself:** a door you come near is lit and shows the up key; a ladder's foot and top show the key that climbs it – keys, never words. + - **Rooms move:** painted lanterns flicker with dust in their light, the pond above drips through the ceiling (`lamp` and `drip` spawn markers, `stage/ambient.ts`). + - **Second pass (same evening):** a ledge that steps down under an arch is measured as it steps (he stood on air); the top of a back wall under the water ceiling is no floor (he hovered at a pillar); where a ladder passes through a ledge the ledge's face is drawn over the climber (`LEDGE_FACE`, in `06-pillars`), so he climbs down behind the stone; the climb is never mirrored (the clip alternates its own arms, and a mirrored back reads THD as DHT); each room's Dexter sighting stands where you see it from the way in. - **Depth underground has no parallax:** the grown row's masks (`depth_layers.py masks … --all`, `masks/`, `previews/depth-below.png`) are water, play and foreground, everything else the back wall; a `below` chunk writes no sky, far or mid (the pond scenes' refine scattered specks of them). The key says what is navy: only the water on top – asked loosely, the model painted the cellar's dark rooms as water. Keep the better key per chunk (`tmp/boards//key-try*/`). - **A band grows from what reaches it:** "no trees" in the sky brief cut the big tree's crown flat at the seam; the brief says what continues up from below. - **No band above the pond:** girders over the garden read out of context (Alexander, 24 Oct); the zone grows down, into built campus space. diff --git a/game/src/core/scaffold.ts b/game/src/core/scaffold.ts index 79e8c59..75a7c1a 100644 --- a/game/src/core/scaffold.ts +++ b/game/src/core/scaffold.ts @@ -14,6 +14,7 @@ import { BODY_WIDTH } from '../player/body'; import { Triggers } from '../stage/triggers'; import { Rooms } from '../stage/rooms'; import { Props } from '../stage/props'; +import { Ambient } from '../stage/ambient'; import { addChickens } from '../stage/chicken'; import { DebugOverlay, type EnemyLike, type JennyLike, type DexterLike } from '../stage/debugOverlay'; import { fallCapture } from '../stage/fallCapture'; @@ -66,6 +67,7 @@ export class Stage extends Phaser.Scene { this.triggers = new Triggers(data.collisions, data.manifest.region); this.rooms = new Rooms(this, this.player, data.collisions, data.manifest.region); this.props = new Props(this, data.collisions, () => this.player?.body, data.state); + new Ambient(this, data.collisions); addChickens(this, data.collisions); this.follow = import.meta.env.DEV && new URLSearchParams(location.search).has('inspect') ? undefined : cameraFollow(this, data.manifest); diff --git a/game/src/player/player.ts b/game/src/player/player.ts index 19d05a9..8213da4 100644 --- a/game/src/player/player.ts +++ b/game/src/player/player.ts @@ -3,7 +3,7 @@ import type { CollisionChunk, Box } from '../stage/collision'; import { BODY_HEIGHT, BODY_WIDTH, overlaps, stepBody, type Body } from './body'; import { horizontal, move, movement, RUN_SPEED, WALK_SPEED } from './movement'; import { jump, jumpState, JUMP_SPEED, landing, LANDING_TIMES, pogo, type LandingTimes } from './jump'; -import { climb, climbFlip, dropThrough, fallLine, surfaceNear, takeLadderEnd, traversal } from './traversal'; +import { climb, dropThrough, fallLine, surfaceNear, takeLadderEnd, traversal } from './traversal'; import { catalogue, clipLength, LocomotionAnimation, type Catalogue, type MotionPose } from './animation'; import { gameEvents } from '../core/events'; import { Life } from '../combat/life'; @@ -300,7 +300,7 @@ export class Player { sway: this.sway.update(this.wind, this.body.x, this.scene.game.loop.time / 1000, milliseconds / 1000, standing) }; if (this.scene.textures.get(pose.board).has(pose.frame)) this.sprite.setTexture(pose.board, pose.frame); else this.sprite.setTexture('hero-move', 'idle-00'); - this.sprite.setFlipX(this.traversal.ladder && pose === locomotion ? climbFlip(this.body.y) : this.motion.facing < 0).setAlpha(this.hurt.alpha(this.life, this.scene.time.now)); + this.sprite.setFlipX(this.traversal.ladder && pose === locomotion ? false : this.motion.facing < 0).setAlpha(this.hurt.alpha(this.life, this.scene.time.now)); if (this.dodge.active) this.dodgePose(milliseconds / 1000); this.feel(pose, milliseconds / 1000); this.settle(); diff --git a/game/src/player/traversal.test.ts b/game/src/player/traversal.test.ts index ca0f693..3eb40b7 100644 --- a/game/src/player/traversal.test.ts +++ b/game/src/player/traversal.test.ts @@ -74,15 +74,10 @@ describe('vertical traversal', () => { }); }); -import { climbFlip as flip, RUNG_STEP as STEP } from './traversal'; -describe('alternating climb (playtest 24 Oct, M2)', () => { - it('mirrors the climb sprite every rung step, and holds still when he stops', () => { - expect(STEP).toBe(35); - expect(flip(0)).toBe(false); - expect(flip(STEP)).toBe(true); - expect(flip(2 * STEP)).toBe(false); - expect(flip(STEP + 10)).toBe(flip(STEP + 20)); - expect(flip(-1)).toBe(true); +import heroAir from '../../public/assets/sprites/hero-air.anims.json'; +describe('the climb (Alexander, 25 Oct)', () => { + it('alternates in its own clip, so it is never mirrored (a mirrored back reads THD as DHT)', () => { + expect(heroAir.ladder.frames).toBe(4); }); }); diff --git a/game/src/player/traversal.ts b/game/src/player/traversal.ts index 1fce683..7c654e1 100644 --- a/game/src/player/traversal.ts +++ b/game/src/player/traversal.ts @@ -2,14 +2,9 @@ import type { Box, Geometry } from '../stage/collision'; import type { Body } from './body'; export const CLIMB_SPEED = 70; /** - * One rung step of the climb (px): the distance one pass of the 4-frame, 8 fps ladder clip covers at CLIMB_SPEED. - * The climb sprite mirrors every step, so the arms alternate up the ladder without new art (playtest 24 Oct, M2). + * The climb is never mirrored: the ladder clip already alternates the arms (frame 2 reaches with the left), and a + * mirrored back reads the poncho's THD as DHT (Alexander's playtest, 25 Oct). It replaced the rung-step flip of 24 Oct. */ -export const RUNG_STEP = (CLIMB_SPEED * 4) / 8; -/** Whether the climb sprite is mirrored with the feet at height `y`: it flips each rung step, and holds when he stops. */ -export function climbFlip(y: number): boolean { - return ((Math.floor(y / RUNG_STEP) % 2) + 2) % 2 === 1; -} /** * Getting on and off a ladder (Alexander, 24 Oct): the hero-ladder board's clips, played forward when he leaves the * ladder and reversed when he takes it. ladder-top: hauled up over the deck edge to a stand; ladder-bottom: off the diff --git a/game/src/stage/ambient.test.ts b/game/src/stage/ambient.test.ts new file mode 100644 index 0000000..d80683c --- /dev/null +++ b/game/src/stage/ambient.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; +import { flicker, landing } from './ambientModel'; + +describe('ambient: the rooms move', () => { + it('drops a drip onto the first floor under it, else the room bottom', () => { + const lines = [[[0, 342], [2020, 342]], [[0, 601], [2160, 607]]] as [number, number][][]; + expect(landing(lines, 300, 104, 1440)).toBe(342); + expect(landing(lines, 300, 400, 1440)).toBeCloseTo(601.8, 0); + expect(landing(lines, 2100, 104, 1440)).toBeCloseTo(606.8, 0); // past the ledge's end + expect(landing([], 300, 104, 1440)).toBe(1440); + }); + it('keeps a lantern between 0.55 and 1, and moving', () => { + const values = Array.from({ length: 600 }, (_, i) => flicker(i / 60, 0.4)); + expect(Math.min(...values)).toBeGreaterThanOrEqual(0.55); + expect(Math.max(...values)).toBeLessThanOrEqual(1); + expect(new Set(values.map(v => v.toFixed(2))).size).toBeGreaterThan(5); + }); +}); diff --git a/game/src/stage/ambient.ts b/game/src/stage/ambient.ts new file mode 100644 index 0000000..25c5706 --- /dev/null +++ b/game/src/stage/ambient.ts @@ -0,0 +1,97 @@ +import Phaser from 'phaser'; +import type { CollisionChunk, Marker } from './collision'; +import { PROP_DEPTH } from './depth'; +import { DRIP, flicker, LAMP, landing } from './ambientModel'; + +/** + * The rooms' small motion (Alexander's playtest, 25 Oct: the underground was "extremely static"): every `lamp` + * spawn marker is a painted lantern that flickers, with dust drifting in its light; every `drip` marker is where the + * pond above seeps through the ceiling – a drop forms, falls to the floor under it and splashes. Markers come from + * the zone script (hidden_garden_proof.py AMBIENT); the painting itself never moves. + */ +export class Ambient { + private lamps: { at: Marker; glow: Phaser.GameObjects.Image; motes: Phaser.GameObjects.Image[]; seed: number }[] = []; + private drips: { at: Marker; land: number; next: number; drop?: Phaser.GameObjects.Image; vy: number }[] = []; + + constructor(private scene: Phaser.Scene, chunks: CollisionChunk[]) { + const spawns = chunks.flatMap(c => c.world.spawn.map(s => ({ s, c }))); + const lamps = spawns.filter(({ s }) => s.kind === 'lamp'); + const drips = spawns.filter(({ s }) => s.kind === 'drip'); + if (!lamps.length && !drips.length) return; + textures(scene); + for (const [n, { s }] of lamps.entries()) { + const glow = scene.add.image(s.x, s.y, 'ambient-glow').setBlendMode(Phaser.BlendModes.ADD).setTint(LAMP.tint) + .setScale(LAMP.radius / 64).setDepth(PROP_DEPTH - 2); + const motes = Array.from({ length: LAMP.motes }, () => + scene.add.image(s.x, s.y, 'ambient-mote').setTint(0xffe2b0).setDepth(PROP_DEPTH - 1)); + this.lamps.push({ at: s, glow, motes, seed: n * 1.37 + 0.4 }); + } + for (const { s, c } of drips) { + const lines = [...c.world.walk, ...c.world.oneWay]; + const bottom = c.origin[1] + c.local.size[1]; + this.drips.push({ at: s, land: landing(lines, s.x, s.y, bottom), next: Math.random() * DRIP.every[1], vy: 0 }); + } + const update = (time: number, delta: number) => this.update(time / 1000, Math.min(delta, 100) / 1000); + scene.events.on(Phaser.Scenes.Events.UPDATE, update); + scene.events.once(Phaser.Scenes.Events.SHUTDOWN, () => scene.events.off(Phaser.Scenes.Events.UPDATE, update)); + } + + private update(t: number, dt: number) { + for (const lamp of this.lamps) { + const light = flicker(t, lamp.seed); + lamp.glow.setAlpha(LAMP.alpha * light); + lamp.motes.forEach((mote, i) => { + const p = lamp.seed * 7 + i * 2.1; + mote.setPosition(lamp.at.x + LAMP.drift[0] * Math.sin(t * 0.11 + p), lamp.at.y + 30 + LAMP.drift[1] * Math.sin(t * 0.07 + p * 1.3)) + .setAlpha(0.45 * light * (0.5 + 0.5 * Math.sin(t * 0.9 + p))); + }); + } + for (const drip of this.drips) { + if (!drip.drop) { + drip.next -= dt; + if (drip.next > 0) continue; + drip.drop = this.scene.add.image(drip.at.x, drip.at.y, 'ambient-drop').setTint(DRIP.tint).setDepth(PROP_DEPTH - 1); + drip.vy = 0; + continue; + } + drip.vy += DRIP.gravity * dt; + drip.drop.y += drip.vy * dt; + if (drip.drop.y >= drip.land) { + this.splash(drip.at.x, drip.land); + drip.drop.destroy(); + drip.drop = undefined; + drip.next = DRIP.every[0] + Math.random() * (DRIP.every[1] - DRIP.every[0]); + } + } + } + + private splash(x: number, y: number) { + for (let i = 0; i < DRIP.splash; i++) { + const bit = this.scene.add.image(x, y - 1, 'ambient-mote').setTint(DRIP.tint).setDepth(PROP_DEPTH - 1); + this.scene.tweens.add({ targets: bit, x: x + (i - 1) * 7, y: y - 6 - 3 * (i % 2), alpha: 0, duration: 320, + ease: 'Quad.easeOut', onComplete: () => bit.destroy() }); + } + } +} + +/** The glow (a soft radial falloff), a dust mote (2 px) and a drop (2 x 5 px), made once per game. */ +function textures(scene: Phaser.Scene) { + if (!scene.textures.exists('ambient-glow')) { + const canvas = scene.textures.createCanvas('ambient-glow', 128, 128)!; + const ctx = canvas.getContext(); + const gradient = ctx.createRadialGradient(64, 64, 0, 64, 64, 64); + gradient.addColorStop(0, 'rgba(255,255,255,1)'); + gradient.addColorStop(0.35, 'rgba(255,255,255,0.45)'); + gradient.addColorStop(1, 'rgba(255,255,255,0)'); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, 128, 128); + canvas.refresh(); + } + for (const [key, w, h] of [['ambient-mote', 2, 2], ['ambient-drop', 2, 5]] as const) { + if (scene.textures.exists(key)) continue; + const g = scene.make.graphics({ x: 0, y: 0 }, false); + g.fillStyle(0xffffff, 1).fillRect(0, 0, w, h); + g.generateTexture(key, w, h); + g.destroy(); + } +} diff --git a/game/src/stage/ambientModel.ts b/game/src/stage/ambientModel.ts new file mode 100644 index 0000000..00b28a8 --- /dev/null +++ b/game/src/stage/ambientModel.ts @@ -0,0 +1,25 @@ +import type { Point } from './collision'; + +/** The ambient motion's numbers and pure parts (ambient.ts draws them). */ +export const LAMP = { radius: 120, alpha: 0.3, tint: 0xffb45a, motes: 5, drift: [70, 46] as const }; +export const DRIP = { every: [2.2, 6] as const, gravity: 900, tint: 0xbfe0ff, splash: 3 }; + +/** Where a drop from (x, y) lands: the first walk line under it, else the room's bottom. */ +export function landing(lines: Point[][], x: number, y: number, bottom: number): number { + let best = bottom; + for (const line of lines) for (let i = 1; i < line.length; i++) { + const [a, b] = [line[i - 1], line[i]]; + if (x < Math.min(a[0], b[0]) || x > Math.max(a[0], b[0])) continue; + const ly = a[1] + (b[1] - a[1]) * (x - a[0]) / Math.max(1, b[0] - a[0]); + if (ly > y + 4 && ly < best) best = ly; + } + return best; +} + +/** A lantern's brightness at time t (s), 0.55–1: a slow breath, a quick flutter, and now and then a short dip. */ +export function flicker(t: number, seed: number): number { + const breath = 0.06 * Math.sin(t * 1.7 + seed); + const flutter = 0.05 * Math.sin(t * 13.1 + seed * 3) * Math.sin(t * 7.3 + seed); + const dip = Math.max(0, Math.sin(t * 0.61 + seed * 5) - 0.96) * 9; // about once every ten seconds, ~0.3 s + return Math.max(0.55, Math.min(1, 0.9 + breath + flutter - dip)); +} diff --git a/game/src/stage/collision.test.ts b/game/src/stage/collision.test.ts index 6c637dc..846821e 100644 --- a/game/src/stage/collision.test.ts +++ b/game/src/stage/collision.test.ts @@ -1,5 +1,5 @@ import { expect, it, vi } from 'vitest'; -import { loadCollisions, parseCollision, placeCollision, BOXES, LINES, type CollisionData } from './collision'; +import { edgeWalls, loadCollisions, parseCollision, placeCollision, BOXES, EDGE_WALL, LINES, type CollisionData } from './collision'; import type { StageManifest } from './manifest'; const fixtures = import.meta.glob('../../../art/stages/{rusty-creek,hidden-garden}/*.collision.json', @@ -71,7 +71,9 @@ it('fetches the collision files beside the manifest and builds world coordinates const chunks = await loadCollisions(manifest(), 'anomaly', request); expect(request).toHaveBeenCalledWith('/assets/stages/rusty-creek/rc-0-0.anomaly.collision.json'); expect(chunks[1].origin).toEqual([2160, 0]); - for (const key of BOXES) expect(chunks[1].world[key]).toHaveLength(chunks[1].local[key].length); + // every authored box is placed; the loader only adds the walls that close the row's open ends (edgeWalls) + for (const key of BOXES) expect(chunks[1].world[key].filter(b => b.kind !== 'edge')).toHaveLength(chunks[1].local[key].length); + expect(chunks[1].world.solid.filter(b => b.kind === 'edge').map(b => b.x)).toEqual([4320]); }); it('refuses a missing collision file instead of inferring physics from artwork', async () => { @@ -80,3 +82,14 @@ it('refuses a missing collision file instead of inferring physics from artwork', const input = manifest(); input.chunks[0].states.anomaly!.collision = null; await expect(loadCollisions(input, 'anomaly', request)).rejects.toThrow('Missing collision:'); }); + +it("closes each row where it has no neighbour: the cellar under the shed is its row's left end", () => { + const placement = [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }]; + const chunks = placement.map(p => placeCollision(fixture('hg-2-1', 'clean'), [p.x * 2160, p.y * 720])); + edgeWalls(chunks, placement, [2160, 720]); + const edges = chunks.map(c => c.world.solid.filter(s => s.kind === 'edge').map(s => s.x)); + expect(edges[0]).toEqual([-EDGE_WALL]); // the row's left end, nothing to its right is open + expect(edges[1]).toEqual([4320]); // the row's right end + expect(edges[2]).toEqual([2160 - EDGE_WALL, 4320]); // alone in its row: both ends + expect(chunks[2].world.solid.every(s => s.kind !== 'edge' || s.h === 720 && s.y === 720)).toBe(true); +}); diff --git a/game/src/stage/collision.ts b/game/src/stage/collision.ts index 21d673b..c506368 100644 --- a/game/src/stage/collision.ts +++ b/game/src/stage/collision.ts @@ -97,8 +97,32 @@ export function placeCollision(local: CollisionData, origin: Point): CollisionCh return { local, origin, world }; } +/** How thick (px) the wall is that closes a row where it has no neighbouring chunk. */ +export const EDGE_WALL = 48; + +/** + * Close every row's open ends: where a chunk has no neighbour to its left or right in its row, a full-height solid + * just outside it (kind "edge"), so the hero can't walk off the painted world (Alexander's playtest, 25 Oct: the + * cellar under the shed is the left end of its row, and walking left fell out of the map). + */ +export function edgeWalls(chunks: CollisionChunk[], placement: { x: number; y: number }[], size: [number, number]): void { + const at = new Set(placement.map(p => `${p.x},${p.y}`)); + chunks.forEach((chunk, i) => { + const { x, y } = placement[i]; + const [ox, oy] = chunk.origin; + if (!at.has(`${x - 1},${y}`)) chunk.world.solid.push({ x: ox - EDGE_WALL, y: oy, w: EDGE_WALL, h: size[1], kind: 'edge', id: `${chunk.local.chunk}-edge-left` }); + if (!at.has(`${x + 1},${y}`)) chunk.world.solid.push({ x: ox + size[0], y: oy, w: EDGE_WALL, h: size[1], kind: 'edge', id: `${chunk.local.chunk}-edge-right` }); + }); +} + export async function loadCollisions(manifest: StageManifest, state: RegionState, request: typeof fetch = (...args) => fetch(...args)): Promise { + const chunks = await loadPlaced(manifest, state, request); + edgeWalls(chunks, manifest.chunks, manifest.chunkSize); + return chunks; +} + +function loadPlaced(manifest: StageManifest, state: RegionState, request: typeof fetch): Promise { return Promise.all(manifest.chunks.map(async chunk => { const file = chunk.states[state]?.collision; if (!assetFile(file)) throw new Error(`Missing collision: ${manifest.region}/${chunk.id}.${state}`); diff --git a/game/src/stage/debugOverlay.ts b/game/src/stage/debugOverlay.ts index 4fd5730..03c90ac 100644 --- a/game/src/stage/debugOverlay.ts +++ b/game/src/stage/debugOverlay.ts @@ -32,7 +32,7 @@ export interface JennyLike { x: number; phase: string; facing: number; health: number; active: boolean; arena: { left: number; right: number; floor: number }; bounds: { x: number; y: number; w: number; h: number }; } -export interface DexterLike { x: number; state: string; stop?: { x: number }; route?: unknown } +export interface DexterLike { x: number; y?: number; state: string; stop?: { x: number }; route?: unknown } /** How far each kind sees ahead (px), from its model's constants; the sight box is one body high. */ export const SIGHT: Record = { rover: SPOT_RANGE, crab: CRAB_SPOT, drone: DRONE_SPOT }; @@ -125,7 +125,8 @@ export class DebugOverlay { } const dexter = this.sources.dexter(); if (dexter && hero) { - const y = groundAt(this.chunks, dexter.x, hero.y); + // at his own height when he reports it (the guide stays on his row); else the ground under him nearest the hero + const y = typeof dexter.y === 'number' ? dexter.y : groundAt(this.chunks, dexter.x, hero.y); g.lineStyle(2, 0x60ff90).strokeRect(dexter.x - 12, y - 30, 24, 30); if (dexter.stop) { g.lineStyle(1, 0x60ff90, 0.8).beginPath().moveTo(dexter.x, y - 15).lineTo(dexter.stop.x, y - 15).strokePath(); diff --git a/game/src/stage/rooms.test.ts b/game/src/stage/rooms.test.ts index 845fd13..629aad1 100644 --- a/game/src/stage/rooms.test.ts +++ b/game/src/stage/rooms.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DoorUse, doorProblems, nearNote } from './rooms'; +import { DoorUse, doorProblems, ladderHint, nearDoor, nearNote } from './rooms'; import type { Box } from './collision'; import type { Body } from '../player/body'; @@ -38,3 +38,17 @@ describe('rooms: notes', () => { expect(nearNote(at(530, 300), [note])).toBeUndefined(); // the storey above }); }); + +describe('rooms: what can be used shows itself', () => { + it('lights a door you come up to on its floor, not one above or below you', () => { + expect(nearDoor(at(3150, 1076), [shed, cellar])).toBe(cellar); + expect(nearDoor(at(3300, 1076), [shed, cellar])).toBeUndefined(); + expect(nearDoor(at(3040, 900), [shed, cellar])).toBeUndefined(); + }); + it("shows the climb at a ladder's foot and top, not beside it", () => { + const ladder: Box = { kind: 'ladder', x: 447, y: 342, w: 35, h: 261 }; + expect(ladderHint(at(464, 603), [ladder])).toMatchObject({ up: true }); + expect(ladderHint(at(464, 342), [ladder])).toMatchObject({ up: false }); + expect(ladderHint(at(600, 603), [ladder])).toBeUndefined(); + }); +}); diff --git a/game/src/stage/rooms.ts b/game/src/stage/rooms.ts index 3847430..262cd53 100644 --- a/game/src/stage/rooms.ts +++ b/game/src/stage/rooms.ts @@ -2,7 +2,7 @@ import type Phaser from 'phaser'; import type { Box, CollisionChunk } from './collision'; import { overlaps, type Body } from '../player/body'; import type { Player } from '../player/player'; -import { CODES, physicalKeys } from '../player/physicalKeys'; +import { CODES, keyName, physicalKeys } from '../player/physicalKeys'; import { gameEvents } from '../core/events'; import { PROP_DEPTH } from './depth'; @@ -13,10 +13,17 @@ import { PROP_DEPTH } from './depth'; * Rooms are grid rows (camera.ts roomBounds), so a door is how you go from the garden into its cellar and back. * - `note`: `title` and `text`; a glint marks it, ↑ reads it, walking away (or ↑ again) puts it back. * Up is edge-triggered, as at the coffee machine, so holding it through a door does not walk you back. + * What can be used shows itself before you reach it (Alexander's playtest, 25 Oct: the cellar's door read as a + * purple box and nothing): a door you come near is lit warm and shows the up key, dim until you stand in it; at a + * ladder's foot or top the key that climbs it. Still wordless – the keys you bound, never a verb. */ export const DOOR_FADE = 220; /** How close (px, centre to body) the note prompt shows, and how far you can walk before a note closes. */ export const NOTE_RANGE = 90; +/** How close (px, door centre to body) a door lights up and shows its key. */ +export const DOOR_RANGE = 160; +/** How close (px, feet to a ladder's end) the ladder shows the key that climbs it. */ +export const LADDER_REACH = 28; /** The door the body stands in when up is pressed (edge), and where it leads: the other door's floor, centred. */ export class DoorUse { @@ -42,6 +49,21 @@ export function nearNote(body: Body, notes: Box[]): Box | undefined { return notes.find(n => Math.abs(body.x - (n.x + n.w / 2)) < NOTE_RANGE && body.y > n.y - 20 && body.y < n.y + n.h + 40); } +/** The door the body is coming up to on its floor: centre within DOOR_RANGE, feet at the door's foot. */ +export function nearDoor(body: Body, doors: Box[]): Box | undefined { + return doors.find(d => Math.abs(body.x - (d.x + d.w / 2)) < DOOR_RANGE && Math.abs(body.y - (d.y + d.h)) < 40); +} + +/** Standing at a ladder's foot (it climbs up) or its top (it climbs down), not on it: where and which way. */ +export function ladderHint(body: Body, climbs: Box[]): { x: number; y: number; up: boolean } | undefined { + for (const c of climbs) { + if (body.x < c.x - 6 || body.x > c.x + c.w + 6) continue; + if (Math.abs(body.y - (c.y + c.h)) < LADDER_REACH) return { x: c.x + c.w / 2, y: body.y, up: true }; + if (Math.abs(body.y - c.y) < LADDER_REACH) return { x: c.x + c.w / 2, y: body.y, up: false }; + } + return undefined; +} + export class Rooms { private doors: Box[]; private notes: Box[]; @@ -54,17 +76,22 @@ export class Rooms { private open?: Box; private read = new Set(); private glints = new Map(); + private climbs: Box[]; + private light: Phaser.GameObjects.Graphics; private travelling = false; constructor(private scene: Phaser.Scene, private player: Player, chunks: CollisionChunk[], private region: string) { const boxes = chunks.flatMap(chunk => chunk.world.interact); this.doors = boxes.filter(box => box.kind === 'door'); this.notes = boxes.filter(box => box.kind === 'note'); + this.climbs = chunks.flatMap(chunk => chunk.world.climb); + this.light = scene.add.graphics().setDepth(PROP_DEPTH).setBlendMode(1); // ADD: warm light spilling from the door for (const problem of doorProblems(this.doors)) console.warn(`${region}: ${problem}`); this.keys = scene.input.keyboard!.addKeys('W,UP') as Record; this.physical = physicalKeys(scene); - this.prompt = scene.add.text(0, 0, '', { fontFamily: 'monospace', fontSize: '13px', color: '#fff3d6', - backgroundColor: '#2a1a0ecc', padding: { x: 6, y: 3 } }).setOrigin(0.5, 1).setDepth(10002).setVisible(false); + // A keycap: big enough to read at a glance over the painted room (the 13 px tag went unseen in the playtest). + this.prompt = scene.add.text(0, 0, '', { fontFamily: 'monospace', fontSize: '18px', fontStyle: 'bold', color: '#fff3d6', + backgroundColor: '#2a1a0ee6', padding: { x: 9, y: 5 } }).setOrigin(0.5, 1).setDepth(10002).setVisible(false); // A sheet of paper held up to the screen, under the HUD: the camera's, not the world's; never over the hero's floor. this.panel = scene.add.text(scene.cameras.main.width / 2, 96, '', { fontFamily: 'monospace', fontSize: '14px', color: '#2b2118', backgroundColor: '#efe4c8f2', lineSpacing: 4, @@ -84,12 +111,25 @@ export class Rooms { if (this.open && (this.open !== note || pressed && !through)) this.close(); else if (note && pressed && !through && !this.open) this.show(note); const door = free ? this.doors.find(box => overlaps(body, box)) : undefined; - const at = door ?? (this.open ? undefined : note); - this.prompt.setVisible(!!at); + const coming = free && !door ? nearDoor(body, this.doors) : undefined; + const ladder = free && !door && !note ? ladderHint(body, this.climbs) : undefined; + const at = door ?? coming ?? (this.open ? undefined : note); + const bob = 3 * Math.sin(this.scene.time.now / 300); + const upKey = keyName(CODES.up[0] ?? 'ArrowUp'); + this.light.clear(); + const lit = door ?? coming; + if (lit) { + const glow = (door ? 0.2 : 0.11) + 0.04 * Math.sin(this.scene.time.now / 420); + this.light.fillStyle(0xffc46b, glow).fillRect(lit.x - 6, lit.y - 30, lit.w + 12, lit.h + 30); + } + this.prompt.setVisible(!!at || !!ladder); if (at) { // A door says nothing, only the key: the garden's prologue is wordless (docs/03). A note is read. - this.prompt.setText(door ? '↑' : '↑ read'); - this.prompt.setPosition(Math.round(at.x + at.w / 2), Math.round(at.y - 10 + 3 * Math.sin(this.scene.time.now / 300))); + this.prompt.setText(at === note && !door && !coming ? `${upKey} read` : upKey).setAlpha(coming ? 0.6 : 1); + this.prompt.setPosition(Math.round(at.x + at.w / 2), Math.round(at.y - 34 + bob)); + } else if (ladder) { + this.prompt.setText(ladder.up ? `${upKey} ↑` : `${keyName(CODES.down[0] ?? 'ArrowDown')} ↓`).setAlpha(0.85); + this.prompt.setPosition(Math.round(ladder.x), Math.round(ladder.y - 96 + bob)); } } diff --git a/game/src/story/Prologue.ts b/game/src/story/Prologue.ts index cffa12b..4c7b9c0 100644 --- a/game/src/story/Prologue.ts +++ b/game/src/story/Prologue.ts @@ -7,7 +7,7 @@ import type { Box, CollisionChunk, Marker } from '../stage/collision'; import { createAnims, type AtlasAnim } from './atlases'; import type { CrossingHero } from './Crossing'; import { FINALE, FinaleSequence, knockArc, type FinalePhase } from './prologueFinale'; -import { DexterGuide, stopsFrom, type GuideAnim, type GuideView, type Pt } from './prologueGuide'; +import { DexterGuide, guideLines, stopsFrom, type GuideAnim, type GuideView, type Pt } from './prologueGuide'; const ANIMS: AtlasAnim[] = [ ['dexter', 'idle', 3, -1], ['dexter', 'hop', 4, -1], ['dexter', 'walk', 6, -1], ['dexter', 'point', 3, 0], @@ -77,10 +77,12 @@ export class Prologue { constructor(private readonly stage: StageScene, chunks: CollisionChunk[], private readonly region: string, private readonly crash: Crash404) { const worlds = chunks.map(c => c.world); - const lines: Pt[][] = worlds.flatMap(w => [...w.walk, ...w.oneWay]); const boxes = worlds.flatMap(w => w.interact); const spawns = worlds.flatMap(w => w.spawn); const bench = spawns.find(s => s.id === 'dexter-bench') ?? spawns.find(s => s.kind === 'dexter')!; + // He guides along his own row only: given the rooms under it too, he followed the hero down into the cellar + // and walked on its floors and through its air (Alexander's playtest, 25 Oct). Rows are rooms (camera.ts). + const lines: Pt[][] = guideLines(chunks, bench.y); // The hop's length is the dexter board's hop clip (#156: one timing source). const hop = (stage.cache.json.get('dexter.anims') as Record | undefined)?.hop; this.guide = new DexterGuide({ bench, lines, ...stopsFrom(boxes), ...(hop && { hopClip: hop.frames / hop.fps * 1000 }) }); diff --git a/game/src/story/prologueGuide.test.ts b/game/src/story/prologueGuide.test.ts index bf6fe19..bbe9fbb 100644 --- a/game/src/story/prologueGuide.test.ts +++ b/game/src/story/prologueGuide.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { RUN_SPEED, WALK_SPEED } from '../player/movement'; -import { DexterGuide, GUIDE, PLUSH, Route, stopsFrom, type GuideView, type Pt } from './prologueGuide'; +import { DexterGuide, GUIDE, guideLines, PLUSH, Route, stopsFrom, type GuideView, type Pt } from './prologueGuide'; interface Chunk { chunk: string; walk: Pt[][]; oneWay: Pt[][]; interact: { id: string; kind: string; act?: string; x: number; y: number; w: number; h: number }[]; spawn: { id: string; kind: string; x: number; y: number }[] } const files = import.meta.glob('../../../art/stages/hidden-garden/hg-*.clean.collision.json', { eager: true, import: 'default' }) as Record; @@ -246,3 +246,12 @@ describe('Dexter leading', () => { }); }); + +describe('his ground', () => { + it('is his own row only, never the rooms under it', () => { + const chunk = (y: number, line: Pt[]) => ({ origin: [0, y] as [number, number], local: { size: [2160, 720] }, + world: { walk: [line], oneWay: [] } }) as never; + const lines = guideLines([chunk(0, [[0, 456], [2160, 456]]), chunk(720, [[0, 1062], [2160, 1062]])], 456); + expect(lines).toEqual([[[0, 456], [2160, 456]]]); + }); +}); diff --git a/game/src/story/prologueGuide.ts b/game/src/story/prologueGuide.ts index a90a107..296f1b8 100644 --- a/game/src/story/prologueGuide.ts +++ b/game/src/story/prologueGuide.ts @@ -1,3 +1,4 @@ +import type { CollisionChunk } from '../stage/collision'; import { RUN_SPEED, WALK_SPEED } from '../player/movement'; /** @@ -394,3 +395,10 @@ export function stopsFrom(boxes: { id?: string; kind?: string; act?: unknown; x: })); return { wakeX: wake ? wake.x : -Infinity, stops }; } + +/** The walk and one-way lines of the row (chunks sharing an origin y) that holds the y given: the guide's ground. */ +export function guideLines(chunks: Pick[], y: number): Pt[][] { + const height = chunks[0]?.local.size[1] ?? 720; + const row = chunks.filter(c => y >= c.origin[1] && y < c.origin[1] + height); + return row.flatMap(c => [...c.world.walk, ...c.world.oneWay]); +} diff --git a/game/verification/underground/behind-pillar.png b/game/verification/underground/behind-pillar.png new file mode 100644 index 0000000..d56a79a Binary files /dev/null and b/game/verification/underground/behind-pillar.png differ diff --git a/game/verification/underground/door-lit.png b/game/verification/underground/door-lit.png new file mode 100644 index 0000000..ecc73a2 Binary files /dev/null and b/game/verification/underground/door-lit.png differ diff --git a/game/verification/underground/ladder-key.png b/game/verification/underground/ladder-key.png new file mode 100644 index 0000000..a92dbf8 Binary files /dev/null and b/game/verification/underground/ladder-key.png differ diff --git a/game/verification/underground/ledge-in-front.png b/game/verification/underground/ledge-in-front.png new file mode 100644 index 0000000..8de37e4 Binary files /dev/null and b/game/verification/underground/ledge-in-front.png differ diff --git a/game/verification/underground/middle-floor.png b/game/verification/underground/middle-floor.png new file mode 100644 index 0000000..c1dc3ab Binary files /dev/null and b/game/verification/underground/middle-floor.png differ diff --git a/scripts/collision_check.py b/scripts/collision_check.py index 5067d13..d7faff5 100644 --- a/scripts/collision_check.py +++ b/scripts/collision_check.py @@ -36,7 +36,8 @@ BODY = (28, 76) RISE = 88 # highest ledge a held jump reaches, with margin (96 px in docs/04) ACROSS = 140 # widest gap a running jump clears -FLOATS = ("drone", "balloon", "pipe-eyes", "dexter-sighting") # need no reachable floor: they fly, float, or stand +FLOATS = ("drone", "balloon", "pipe-eyes", "dexter-sighting", "lamp", "drip") # need no reachable floor: they fly, +# float, stand, hang on a wall (a lamp) or fall from the ceiling (a drip) # where the hero cannot go (stage/props.ts, story/sightings.ts) SNAP = 6 # ends this close count as one continuous floor STEP_UP = 16 # rises the grounded body walks up without a jump (game/src/player/body.ts) diff --git a/scripts/split_layers.py b/scripts/split_layers.py index d2f0924..3db7ab4 100644 --- a/scripts/split_layers.py +++ b/scripts/split_layers.py @@ -37,7 +37,8 @@ from PIL import Image, ImageChops, ImageDraw ROOT = Path(__file__).resolve().parents[1] -LAYERS = ("00-sky", "01-far", "02-mid", "03-rear", "04-water", "05-play", "06-near", "07-foreground") +# 06-pillars: columns the hero walks behind (drawn over the actors like 06-near, but still – the near strip sways). +LAYERS = ("00-sky", "01-far", "02-mid", "03-rear", "04-water", "05-play", "06-pillars", "06-near", "07-foreground") def filled(path: Path) -> bool: diff --git a/scripts/zones/hidden_garden_proof.py b/scripts/zones/hidden_garden_proof.py index 11c35de..a6998ce 100644 --- a/scripts/zones/hidden_garden_proof.py +++ b/scripts/zones/hidden_garden_proof.py @@ -116,21 +116,41 @@ def guide(col: int) -> Image.Image: # lies straight under the shed, and its stairs door is right below the shed door. No shaft through the pond: the # pond is the prologue's finale, and the shed door you should not open is the secret (Alexander, 24 Oct). SHED_DOOR = (1095, 1175) +# The cellar's way back up is its big painted wooden door, upstairs in the middle arch (Alexander's playtest, 25 Oct: +# the box used to sit beside it, on a painted ladder, and read as nothing). Paired with the shed door by id. +CELLAR_DOOR = (895, 995) +# Measured where the trace disagrees with the painting (Alexander's playtest, 25 Oct): the cellar's upper ledge is one +# floor behind every pillar, on into the drain's upper ledge; its ladders run from the ledge to the +# floor and no higher (above the ledge they climb into the ceiling girder); the drain's second painted ladder. +# above: traced floors entirely above this y are replaced by `walk`; climb: x0, x1 of ladders from the floor at y +# `top` down to the lowest floor; add: climb boxes x0, y0, x1, y1 added as they are. +# Second playtest (25 Oct): the ledge under the middle arch is 15 px lower than at the sides (he stood on air there), +# and the drain's top "floor" was the top of its back wall under the water ceiling (he hovered at the pillar): gone, +# with every ladder trimmed to the floors it joins. `drop`: traced floors entirely above this y go. +MEASURED = { + "hgz-0-1": {"above": 500, "walk": [[[0, 342], [720, 342], [760, 357], [1140, 357], [1180, 342], [2160, 342]]], + "top": 342, "climb": [(447, 482), (1817, 1850)]}, + "hgz-1-1": {"drop": 200, "add": [(358, 342, 396, 642)]}, +} +# Where a ladder passes through the cellar's upper ledge, the ledge's face is in front of the climber (he climbs +# down behind the stone, not over it): x0, x1 per zone chunk, the face's rows under the floor. +LEDGE_FACE = {"hgz-0-1": [(432, 497), (1802, 1865)]} +LEDGE_ROWS = (348, 402) GIRDER_TOP = 400 # a traced floor above this is a girder hanging from the water (the reflection) BODY = 76 # the hero's body height: a ladder top keeps it below the water ceiling # What you find down there (docs/03: archive notes hint at the experiment and never explain it; the rest is wordless). # (col, id, x centre, which floor: "low" / "high" (the one above the lowest) / "top", title, text) – placed on the traced floor. FINDS = [ - (0, "q7-sheet-3", 980, "low", "FELDVERSUCH Q-7 – Protokoll, Blatt 3", + (0, "q7-sheet-3", 230, "low", "FELDVERSUCH Q-7 – Protokoll, Blatt 3", "Coherence holds 11 s. The pond shows the campus twice.\nIn the second copy the rovers are already awake.\n" "Containment vessel ordered: 1 x mascot-shaped, Campus Store, 1.5 ft. It fits."), - (0, "q7-margin", 2080, "top", "(pencil, on the back of Blatt 3)", + (0, "q7-margin", 1380, "top", "(pencil, on the back of Blatt 3)", "It waves.\nNobody taught it to wave."), (1, "drain-report", 1110, "low", "Stadtentwässerung Deggendorf – Wartungsbericht, Auslass 4", "Grate replaced twice. Opened from the inside both times.\nAt the outlet: 1 paper boat (dry). 1 red balloon, " "resting against the underside of the pond. It does not float up.\nThe outlet hums at night. The campus jingle."), - (1, "balloon-string", 760, "top", "(a string, tied to the rail)", + (1, "balloon-string", 760, "high", "(a string, tied to the rail)", "The string goes up into the water.\nSomething on the other end tugs back. Twice. Friendly."), (2, "q7-sheet-9", 1880, "low", "FELDVERSUCH Q-7 – Blatt 9 (wet)", "Down here everything is the right way round.\nIt is the garden that hangs upside down.\nDo not feed the vessel."), @@ -144,6 +164,16 @@ def guide(col: int) -> Image.Image: (1, "drain-boat", "paper-boat", 960, "low", {}), # washed against the outlet ] ROVER = (1, 640, 1200) +# The rooms move (Alexander's playtest, 25 Oct: "extremely static"): the painted lanterns flicker with dust in their +# light, and the pond above drips through the ceiling. Measured from the paintings: col -> lamps (x, y at the flame), +# drips (x, y where the drop forms; it falls to the floor under it). +AMBIENT = { + 0: {"lamps": [(175, 265), (855, 245), (1335, 280), (2005, 265), (178, 515), (850, 505), (1335, 525), (1995, 515)], + "drips": [(300, 104), (760, 104), (1460, 104), (1900, 104)]}, + 1: {"lamps": [(640, 235), (1170, 235), (130, 560), (635, 560), (1160, 560)], + "drips": [(260, 104), (900, 104), (1380, 104), (1850, 104)]}, + 2: {"lamps": [], "drips": [(330, 162), (650, 312), (1010, 414), (1590, 446), (1720, 262)]}, +} # Dexter in the rooms (docs/03: he acts, never speaks): while he waits at the pond above, the plush is also down # here – one wordless sighting per room, gone when you come close (story/sightings.ts). Ids start with "dexter", @@ -151,7 +181,9 @@ def guide(col: int) -> Image.Image: SIGHTINGS = [ (0, "dexter-cellar", 1500, "top", "beckon", -1), # upstairs in an arch, waving you back toward the stairs (1, "dexter-drain", 1990, "low", "creepy", -1), # at the far end of the drain, a shape too still - (2, "dexter-reflection", 1340, "top", "wait", -1), # on a girder hanging from the water, waiting + # on the hall's floor, in view from where you come in, beckoning you on to the tear (Alexander, 25 Oct: on its + # high girder at the far end he was nowhere to be seen in the hall) + (2, "dexter-reflection", 760, "low", "beckon", -1), ] # The 404 as data (docs/09 stage 6): tears drawn over the clean chunks in the anomaly state, chunk px. The lower row: @@ -215,6 +247,7 @@ def collision(cid: str, col: int, row: int, source: str | None) -> dict: data["climb"] = [box(x0, max(top, ceiling + BODY + 8), x1, bottom, kind="ladder", id=f"{cid}-ladder-{n}") for n, (x0, x1, top, bottom) in enumerate(steps)] data["walk"] = walkable(cid, data["walk"], data["climb"]) + measured(cid, data) data["interact"] = finds(cid, col, data["walk"]) data["spawn"] = [] if col == ROVER[0] and bands is not None: @@ -222,10 +255,39 @@ def collision(cid: str, col: int, row: int, source: str | None) -> dict: data["spawn"] = [{"id": f"{cid}-rover", "kind": "rover", "x": x, "y": floor_at(data["walk"], x, "low"), "patrol": [ROVER[1], ROVER[2]], "note": STORY[col]["story"]}] if bands is not None: - data["spawn"] += props(cid, col, data["walk"]) + sightings(col, data["walk"]) + data["spawn"] += props(cid, col, data["walk"]) + sightings(col, data["walk"]) + ambient(cid, col) return data +def measured(cid: str, data: dict) -> None: + """The MEASURED floors and ladders over the traced ones.""" + fix = MEASURED.get(cid, {}) + if "walk" in fix: + data["walk"] = [l for l in data["walk"] if max(y for _, y in l) >= fix["above"]] + fix["walk"] + if "climb" in fix: + data["climb"] = [box(x0, fix["top"], x1, floor_at(data["walk"], (x0 + x1) // 2, "low"), kind="ladder", + id=f"{cid}-ladder-{n}") for n, (x0, x1) in enumerate(fix["climb"])] + if "drop" in fix: + data["walk"] = [l for l in data["walk"] if max(y for _, y in l) >= fix["drop"]] + trimmed = [] + for c in data["climb"]: + ys = [floor_y for line in data["walk"] for a, b in zip(line, line[1:]) + if a[0] <= c["x"] + c["w"] and b[0] >= c["x"] + and c["y"] - 12 <= (floor_y := round(a[1] + (b[1] - a[1]) * (c["x"] + c["w"] / 2 - a[0]) / max(1, b[0] - a[0]))) <= c["y"] + c["h"] + 12] + if len(ys) >= 2: + trimmed.append({**c, "y": min(ys), "h": max(ys) - min(ys)}) + data["climb"] = trimmed + data["climb"] += [box(x0, y0, x1, y1, kind="ladder", id=f"{cid}-ladder-m{n}") for n, (x0, y0, x1, y1) in + enumerate(fix.get("add", []))] + + +def ambient(cid: str, col: int) -> list[dict]: + """The room's lanterns and drips as spawn markers (the game's stage/ambient.ts animates them).""" + room = AMBIENT.get(col, {}) + return ([{"id": f"{cid}-lamp-{n}", "kind": "lamp", "x": x, "y": y} for n, (x, y) in enumerate(room.get("lamps", []))] + + [{"id": f"{cid}-drip-{n}", "kind": "drip", "x": x, "y": y} for n, (x, y) in enumerate(room.get("drips", []))]) + + def sightings(col: int, walk: list) -> list[dict]: """The room's Dexter sighting as a spawn marker on its traced floor.""" return [{"id": sid, "kind": "dexter-sighting", "x": x, "y": floor_at(walk, x, which), "act": act, "face": face} @@ -249,9 +311,9 @@ def finds(cid: str, col: int, walk: list) -> list[dict]: """The row-below chunk's interacts: the cellar's stairs door (under the shed door) and the archive notes.""" out = [] if col == 0 and walk: - x = (SHED_DOOR[0] + SHED_DOOR[1]) // 2 + x = (CELLAR_DOOR[0] + CELLAR_DOOR[1]) // 2 y = floor_at(walk, x, "high") - out.append(box(SHED_DOOR[0], y - BODY, SHED_DOOR[1], y, kind="door", id="cellar-stairs", to="shed-door", + out.append(box(CELLAR_DOOR[0], y - BODY, CELLAR_DOOR[1], y, kind="door", id="cellar-stairs", to="shed-door", note=STORY[0]["story"])) if col == TURN[0] and walk: x, w, h = TURN[1:] @@ -475,8 +537,8 @@ def graph(collisions: dict[str, dict], grid: dict[str, tuple[int, int]]) -> dict col, row = grid[cid] climb = [[b["x"], b["y"], b["x"] + b["w"], b["y"] + b["h"]] for b in data["climb"]] for door in (b for b in data["interact"] if b.get("kind") == "door"): - climb.append([door["x"], door["y"], door["x"] + door["w"], H] if row == 0 else - [door["x"], 0, door["x"] + door["w"], door["y"] + door["h"]]) + x0, x1 = SHED_DOOR # the pair is joined by id in the game; the graph joins them at the shed door's x + climb.append([x0, door["y"], x1, H] if row == 0 else [x0, 0, x1, door["y"] + door["h"]]) chunks[cid] = {"id": cid, "col": col, "row": row, "band": "level" if row == 0 else "other", "walk": [[a, b] for line in data["walk"] for a, b in zip(line, line[1:])], "oneWay": data["oneWay"], "deck": [], "climb": climb, @@ -515,6 +577,10 @@ def export_stage() -> None: if mask.exists(): shutil.copy(mask, STAGE / "masks" / f"{sid}.{layer}.png") rules.append({"layer": layer, "alpha": f"masks/{sid}.{layer}.png"}) + pillars = pillar_mask(zid) + if pillars is not None: # last rule wins: the column is in front of the floor and the weeds behind it + pillars.save(STAGE / "masks" / f"{sid}.06-pillars.png") + rules.append({"layer": "06-pillars", "alpha": f"masks/{sid}.06-pillars.png"}) layers["chunks"][sid] = {"states": {"clean": f"images/{sid}.png"}, "default": "03-rear", "rules": rules} export["chunks"][sid] = {"x": col + 2, "y": 1} data = json.loads((ZONE / "collision" / f"{zid}.collision.json").read_text(encoding="utf-8")) @@ -535,7 +601,11 @@ def export_stage() -> None: shed["interact"] = [i for i in shed["interact"] if i.get("kind") != "door"] + [shed_door()] (STAGE / "hg-2-0.clean.collision.json").write_text(json.dumps(shed, indent=1, ensure_ascii=False) + "\n", encoding="utf-8") + export["layers"]["06-pillars"] = {"file": "layers/{name}.06-pillars.png", "z": 6, "scroll": 1.0, "optional": True} anomaly_state(export) + for state, order in export["states"].items(): + if "06-pillars" not in order: + order.insert(order.index("06-near"), "06-pillars") for path, data in ((STAGE / "layers.json", layers), (STAGE / "export.json", export)): path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") if "steel" not in surfaces["materials"]: # the reflection's girders (collision_check --surfaces lays the ranges) @@ -601,6 +671,25 @@ def anomaly_state(export: dict) -> None: export["states"]["anomaly"] = clean[:clean.index("05-play") + 1] + ["05-tears"] + clean[clean.index("05-play") + 1:] +# The steel columns the hero walks behind (Alexander's playtest, 25 Oct: "I don't go behind the pillars"), measured +# from the painting: zone chunk -> (x0, x1) per column, and the rows they stand in (the top girder to their base). +PILLARS = {"hgz-0-1": [(42, 88), (375, 425), (655, 708), (1150, 1205), (1675, 1730)]} +PILLAR_ROWS = (60, 615) + + +def pillar_mask(zid: str) -> Image.Image | None: + """The chunk's 06-pillars mask (opaque = the column), or None without columns.""" + if zid not in PILLARS: + return None + mask = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + draw = ImageDraw.Draw(mask) + for x0, x1 in PILLARS[zid]: + draw.rectangle((x0, PILLAR_ROWS[0], x1 - 1, PILLAR_ROWS[1] - 1), fill=(255, 255, 255, 255)) + for x0, x1 in LEDGE_FACE.get(zid, []): + draw.rectangle((x0, LEDGE_ROWS[0], x1 - 1, LEDGE_ROWS[1] - 1), fill=(255, 255, 255, 255)) + return mask + + MASKS = (("04-water", "04-water"), ("05-play", "05-play"), ("07-foreground", "06-near")) # zone band -> stage layer