Spaces:
Running
Running
268252f: Wrong doors wake the owner, and a note on the tree says the rule
Browse files- assets/letter/lit_06.svg +9 -0
- assets/origin_tree/lit_00.svg +6 -0
- assets/tree/lit_00.svg +15 -0
- manifest.json +57 -2
- src/audio.js +5 -4
- src/checks.js +277 -16
- src/drained.js +42 -1
- src/render.js +46 -8
- src/scene_level.js +80 -3
- src/tuning.js +9 -0
assets/letter/lit_06.svg
ADDED
|
|
assets/origin_tree/lit_00.svg
ADDED
|
|
assets/tree/lit_00.svg
ADDED
|
|
manifest.json
CHANGED
|
@@ -315,6 +315,16 @@
|
|
| 315 |
"_letter": "What he was told, in the only voice this game has: palace paper. It may name the errand, the road and the levy, because a palace order would name the levy and nobody will know what that means until village one shows them. It may not say a word about her, about the drained, or about what the levy is. THE SEAL IS THE TITLE CARD -- Astara, once, on the paper, and nowhere else in the game. The newline is load-bearing: wrap() splits on it first, so the seal keeps its own line the way every signature does. 140 characters, against 67 to 149 across the other nine strings.",
|
| 316 |
"fragment": null,
|
| 317 |
"_fragment": "None, and there never will be. Nobody in this village was holding the Crown's order."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
}
|
| 319 |
]
|
| 320 |
},
|
|
@@ -1049,6 +1059,51 @@
|
|
| 1049 |
],
|
| 1050 |
"silhouette": "auto"
|
| 1051 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1052 |
"kept": {
|
| 1053 |
"_note": "FIVE TORN PAGES, AND THEY ARE ALL THE SAME SHAPE. A tool silhouette carries one bit and one bit only supports matching; a line of text carries a relationship, which is what inference needs. So what differs between the five is the scribble, which sits inside the contour and which the silhouette pass discards. A player who could tell them apart by their tear would have the answer without reading a word.",
|
| 1054 |
"path": "assets/kept/",
|
|
@@ -1070,7 +1125,7 @@
|
|
| 1070 |
"_note": "FIVE WHOLE PAGES, and they are the same shape as each other for exactly the reason the torn ones are. Same contour as `kept` with the tear replaced by a straight edge and the same scribble seed, so a fragment reads as a piece off one of these. The sheet carries the shape and the band carries the words; neither does the other's job, and no player ever identifies a house by the outline of its paper.",
|
| 1071 |
"path": "assets/letter/",
|
| 1072 |
"file": "lit_{nn}.svg",
|
| 1073 |
-
"frames":
|
| 1074 |
"fps": 1,
|
| 1075 |
"variants": true,
|
| 1076 |
"native": [
|
|
@@ -1082,7 +1137,7 @@
|
|
| 1082 |
48
|
| 1083 |
],
|
| 1084 |
"silhouette": "auto",
|
| 1085 |
-
"_frames": "Five village papers
|
| 1086 |
},
|
| 1087 |
"key": {
|
| 1088 |
"_note": "ONE KEY, AND EVERY HOUSE HAS THE SAME ONE. Every household in a walled village holds a key to the gate; that is how village gates work. It is not evidence: it never enters the sheaf, it cannot be read, and there is nothing at all to work out about it. All the deduction in this level is in the paper. Visible from the moment a door is lit and not takeable until somebody comes home and opens it β you can see it the whole time and not have it.",
|
|
|
|
| 315 |
"_letter": "What he was told, in the only voice this game has: palace paper. It may name the errand, the road and the levy, because a palace order would name the levy and nobody will know what that means until village one shows them. It may not say a word about her, about the drained, or about what the levy is. THE SEAL IS THE TITLE CARD -- Astara, once, on the paper, and nowhere else in the game. The newline is load-bearing: wrap() splits on it first, so the seal keeps its own line the way every signature does. 140 characters, against 67 to 149 across the other nine strings.",
|
| 316 |
"fragment": null,
|
| 317 |
"_fragment": "None, and there never will be. Nobody in this village was holding the Crown's order."
|
| 318 |
+
},
|
| 319 |
+
{
|
| 320 |
+
"house": "the tree",
|
| 321 |
+
"origin": "tree",
|
| 322 |
+
"_origin": "NOT A HOUSE, like the palace. The scene finds it by this field, and drawLetterHouse draws a tree at the foot of its page.",
|
| 323 |
+
"_points": "AT THE RULE, AND NEVER AT AN ANSWER. It says the drained cannot speak, that some carry something, and that it goes home. Not which ones, not which home, and nothing about him or who wrote it. A rebel's hand, read in the torchlight like everything else.",
|
| 324 |
+
"letter": "They cannot speak.\nWhat they carried is all that is left of them.\nIf you find it, take it home.",
|
| 325 |
+
"_letter": "Three lines, and read on the road, so it is page two after his orders: the palace's instruction and the rebels', which contradict each other. Nothing has to say so.",
|
| 326 |
+
"fragment": null,
|
| 327 |
+
"_fragment": "None. It is not anybody's."
|
| 328 |
}
|
| 329 |
]
|
| 330 |
},
|
|
|
|
| 1059 |
],
|
| 1060 |
"silhouette": "auto"
|
| 1061 |
},
|
| 1062 |
+
"road_tree": {
|
| 1063 |
+
"_what": "THE TREE WITH THE NOTE ON IT. On the road, at his own scale, two-thirds of the way to the village: after the first drained, before the first house. A rebel nailed the rule to it. Placeholder, generated by tools/gen_placeholders.js.",
|
| 1064 |
+
"path": "assets/tree/",
|
| 1065 |
+
"file": "lit_{nn}.svg",
|
| 1066 |
+
"frames": 1,
|
| 1067 |
+
"fps": 1,
|
| 1068 |
+
"native": [
|
| 1069 |
+
420,
|
| 1070 |
+
1180
|
| 1071 |
+
],
|
| 1072 |
+
"pivot": [
|
| 1073 |
+
210,
|
| 1074 |
+
1180
|
| 1075 |
+
],
|
| 1076 |
+
"nailAt": [
|
| 1077 |
+
[
|
| 1078 |
+
185,
|
| 1079 |
+
984,
|
| 1080 |
+
56,
|
| 1081 |
+
76
|
| 1082 |
+
]
|
| 1083 |
+
],
|
| 1084 |
+
"_nailAt": "On the trunk wherever the trunk went, at a house nail's height above the ground, so the torch finds it from where a player already stands to read a door. The generator prints it.",
|
| 1085 |
+
"silhouette": "auto"
|
| 1086 |
+
},
|
| 1087 |
+
"origin_tree": {
|
| 1088 |
+
"_what": "WHERE THE NOTE CAME FROM, drawn at the foot of its page the way the order's page carries the palace. A lone tree.",
|
| 1089 |
+
"path": "assets/origin_tree/",
|
| 1090 |
+
"file": "lit_{nn}.svg",
|
| 1091 |
+
"frames": 1,
|
| 1092 |
+
"fps": 1,
|
| 1093 |
+
"native": [
|
| 1094 |
+
960,
|
| 1095 |
+
900
|
| 1096 |
+
],
|
| 1097 |
+
"pivot": [
|
| 1098 |
+
480,
|
| 1099 |
+
900
|
| 1100 |
+
],
|
| 1101 |
+
"_pivot": "Same canvas and pivot as houses, so drawLetterHouse scales it with the same k.",
|
| 1102 |
+
"bleedEdges": [
|
| 1103 |
+
"bottom"
|
| 1104 |
+
],
|
| 1105 |
+
"silhouette": "auto"
|
| 1106 |
+
},
|
| 1107 |
"kept": {
|
| 1108 |
"_note": "FIVE TORN PAGES, AND THEY ARE ALL THE SAME SHAPE. A tool silhouette carries one bit and one bit only supports matching; a line of text carries a relationship, which is what inference needs. So what differs between the five is the scribble, which sits inside the contour and which the silhouette pass discards. A player who could tell them apart by their tear would have the answer without reading a word.",
|
| 1109 |
"path": "assets/kept/",
|
|
|
|
| 1125 |
"_note": "FIVE WHOLE PAGES, and they are the same shape as each other for exactly the reason the torn ones are. Same contour as `kept` with the tear replaced by a straight edge and the same scribble seed, so a fragment reads as a piece off one of these. The sheet carries the shape and the band carries the words; neither does the other's job, and no player ever identifies a house by the outline of its paper.",
|
| 1126 |
"path": "assets/letter/",
|
| 1127 |
"file": "lit_{nn}.svg",
|
| 1128 |
+
"frames": 7,
|
| 1129 |
"fps": 1,
|
| 1130 |
"variants": true,
|
| 1131 |
"native": [
|
|
|
|
| 1137 |
48
|
| 1138 |
],
|
| 1139 |
"silhouette": "auto",
|
| 1140 |
+
"_frames": "Five village papers, one palace one and one scrap. The sixth is the order, cleaner and squarer because the palace does not write on scraps. The seventh is the note off the tree, torn and in a hand that is not the village's. Every frame stays inside the same ink box: the box is the union of all of them, and one that reached further would shrink every letter on every nail and every page."
|
| 1141 |
},
|
| 1142 |
"key": {
|
| 1143 |
"_note": "ONE KEY, AND EVERY HOUSE HAS THE SAME ONE. Every household in a walled village holds a key to the gate; that is how village gates work. It is not evidence: it never enters the sheaf, it cannot be read, and there is nothing at all to work out about it. All the deduction in this level is in the paper. Visible from the moment a door is lit and not takeable until somebody comes home and opens it β you can see it the whole time and not have it.",
|
src/audio.js
CHANGED
|
@@ -450,7 +450,8 @@ export function createAudio() {
|
|
| 450 |
* what a swing looks like; the clip's name is the whole contract, so a new
|
| 451 |
* loop is a new entry here and nothing else.
|
| 452 |
*/
|
| 453 |
-
drainedVoice: (kind,
|
|
|
|
| 454 |
const pick = { loop_swing: ['drainedSwing', drainedSwing],
|
| 455 |
loop_haul: ['drainedHaul', drainedHaul],
|
| 456 |
loop_bear: ['drainedBear', drainedBear],
|
|
@@ -462,7 +463,7 @@ export function createAudio() {
|
|
| 462 |
// The recording if it has decoded, the generated one if it has not --
|
| 463 |
// the same fallback the boots and the thunder have, and for the same
|
| 464 |
// reason: a recording cannot be fetched before the first gesture.
|
| 465 |
-
if (!playSlice(pick[0], v)) pick[1](c, master, c.currentTime, noise, v);
|
| 466 |
},
|
| 467 |
/**
|
| 468 |
* A key off its hook. Its own sound, because it is its own act.
|
|
@@ -514,11 +515,11 @@ export function createAudio() {
|
|
| 514 |
}
|
| 515 |
burst(c, out, c.currentTime, noise, { freq: 650, q: 0.9, gain: v * 0.5, attack: 0.3, decay: 0.3, sweepTo: 1500 });
|
| 516 |
},
|
| 517 |
-
drainedWake: (
|
| 518 |
if (!enabled) return;
|
| 519 |
const c = ensure();
|
| 520 |
if (!c || c.state !== 'running') return;
|
| 521 |
-
playSlice('wake',
|
| 522 |
},
|
| 523 |
/** The village, under everything. 0..1, set every frame by the scene. */
|
| 524 |
villageBed: (level) => setLoop('villageBed', level * AUDIO.VILLAGE),
|
|
|
|
| 450 |
* what a swing looks like; the clip's name is the whole contract, so a new
|
| 451 |
* loop is a new entry here and nothing else.
|
| 452 |
*/
|
| 453 |
+
drainedVoice: (kind, level = 1, pan = 0) => {
|
| 454 |
+
const v = AUDIO.DRAINED_VOICE * level;
|
| 455 |
const pick = { loop_swing: ['drainedSwing', drainedSwing],
|
| 456 |
loop_haul: ['drainedHaul', drainedHaul],
|
| 457 |
loop_bear: ['drainedBear', drainedBear],
|
|
|
|
| 463 |
// The recording if it has decoded, the generated one if it has not --
|
| 464 |
// the same fallback the boots and the thunder have, and for the same
|
| 465 |
// reason: a recording cannot be fetched before the first gesture.
|
| 466 |
+
if (!playSlice(pick[0], v, pan)) pick[1](c, master, c.currentTime, noise, v);
|
| 467 |
},
|
| 468 |
/**
|
| 469 |
* A key off its hook. Its own sound, because it is its own act.
|
|
|
|
| 515 |
}
|
| 516 |
burst(c, out, c.currentTime, noise, { freq: 650, q: 0.9, gain: v * 0.5, attack: 0.3, decay: 0.3, sweepTo: 1500 });
|
| 517 |
},
|
| 518 |
+
drainedWake: (level = 1, pan = 0) => {
|
| 519 |
if (!enabled) return;
|
| 520 |
const c = ensure();
|
| 521 |
if (!c || c.state !== 'running') return;
|
| 522 |
+
playSlice('wake', AUDIO.WAKE * level, pan);
|
| 523 |
},
|
| 524 |
/** The village, under everything. 0..1, set every frame by the scene. */
|
| 525 |
villageBed: (level) => setLoop('villageBed', level * AUDIO.VILLAGE),
|
src/checks.js
CHANGED
|
@@ -1583,17 +1583,31 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
|
|
| 1583 |
const carriers = armed();
|
| 1584 |
const owner = carriers.find((d) => d.kept === 0); // the forge's
|
| 1585 |
|
| 1586 |
-
// 1. THE WRONG FRAME
|
| 1587 |
-
//
|
|
|
|
|
|
|
|
|
|
| 1588 |
atDoor(1, 0);
|
| 1589 |
tap({ sheaf: true });
|
| 1590 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1591 |
const wrong = {
|
| 1592 |
onTheWall: s.houses[1].filed === 0,
|
| 1593 |
-
|
|
|
|
|
|
|
| 1594 |
nothingOpened: s.houses.every((h) => !h.open),
|
| 1595 |
nothingSettled: s.houses.every((h) => !h.settled),
|
| 1596 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1597 |
|
| 1598 |
// 2. AND IT COMES BACK OUT. Brute force is allowed: four fragments, five
|
| 1599 |
// frames, and nothing stops a player working through them.
|
|
@@ -1605,7 +1619,11 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
|
|
| 1605 |
};
|
| 1606 |
|
| 1607 |
// 3. THE RIGHT FRAME AND HE GETS UP. Nothing is said, here or anywhere.
|
|
|
|
|
|
|
| 1608 |
atDoor(0, 0);
|
|
|
|
|
|
|
| 1609 |
tap({ sheaf: true });
|
| 1610 |
const filed = {
|
| 1611 |
onTheWall: s.houses[0].filed === 0,
|
|
@@ -1625,12 +1643,15 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
|
|
| 1625 |
// 5. IT ARRIVES, AND ONLY ITS OWN DOOR OPENS.
|
| 1626 |
let ticks = 0;
|
| 1627 |
while (owner.state !== 'HOME' && ticks < 60 * 200) { scene.update(1 / 60, NONE); ticks++; }
|
| 1628 |
-
const walked = Math.abs(s.houses[0].x -
|
| 1629 |
const arrived = {
|
| 1630 |
home: owner.state === 'HOME',
|
| 1631 |
atTheDoor: Math.round(owner.x) === s.houses[0].x,
|
| 1632 |
doorsOpen: s.houses.filter((h) => h.open).length,
|
| 1633 |
-
|
|
|
|
|
|
|
|
|
|
| 1634 |
px: Math.round(walked),
|
| 1635 |
// Derived from HOME_SPEED rather than written down, plus the rise.
|
| 1636 |
expected: +(walked / TUNING.HOME_SPEED + TUNING.RISE_MS / 1000).toFixed(1),
|
|
@@ -1645,7 +1666,7 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
|
|
| 1645 |
|
| 1646 |
return {
|
| 1647 |
name: 'walks home',
|
| 1648 |
-
pass: wrong.onTheWall && wrong.
|
| 1649 |
wrong.nothingSettled &&
|
| 1650 |
pulledBack.frameEmpty && pulledBack.stillInTheSheaf &&
|
| 1651 |
filed.onTheWall && filed.settled && filed.gotUp &&
|
|
@@ -1654,7 +1675,7 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
|
|
| 1654 |
arrived.home && arrived.atTheDoor && arrived.doorsOpen === 1 &&
|
| 1655 |
Math.abs(arrived.seconds - arrived.expected) < 1.5 &&
|
| 1656 |
frozen.ignoredTheSword && frozen.didNotMove && frozen.stillHome,
|
| 1657 |
-
|
| 1658 |
andItComesBackOut: pulledBack,
|
| 1659 |
theRightFrameSendsThemHome: filed,
|
| 1660 |
andThenTheFrameIsFinishedWith: settled,
|
|
@@ -2043,9 +2064,12 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
|
|
| 2043 |
at(0); mark();
|
| 2044 |
const mapCannotBeFiled = s.sheaf.index < 0 || filed().join(',') === before2;
|
| 2045 |
|
| 2046 |
-
// 7. NO
|
| 2047 |
-
//
|
| 2048 |
-
//
|
|
|
|
|
|
|
|
|
|
| 2049 |
const noVerdict = !('correct' in s.houses[0]) && !('wrong' in s.houses[0]) &&
|
| 2050 |
s.houses.every((h) => h.filed == null || typeof h.filed === 'number');
|
| 2051 |
|
|
@@ -2456,10 +2480,10 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
|
|
| 2456 |
const highest = rows.length ? Math.min(...rows.map((r) => r.phoneTop)) : 0;
|
| 2457 |
return {
|
| 2458 |
name: 'band fits',
|
| 2459 |
-
//
|
| 2460 |
-
// rather than a floor, so adding a string and not
|
| 2461 |
-
// here rather than on somebody's phone.
|
| 2462 |
-
pass: rows.length ===
|
| 2463 |
strings: rows.length, longest: worst, allowed: BAND.MAX_LINES,
|
| 2464 |
// How far up the scene the tallest band climbs on the narrowest display,
|
| 2465 |
// and how far up it is allowed to. Bigger number is lower on screen.
|
|
@@ -2620,6 +2644,241 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
|
|
| 2620 |
};
|
| 2621 |
}
|
| 2622 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2623 |
// --- the two lanes ------------------------------------------------------------
|
| 2624 |
//
|
| 2625 |
// Every player who saw a Drained on his road fought it: no side scroller lets
|
|
@@ -3014,7 +3273,8 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
|
|
| 3014 |
nothingOutOfReach(), healthCosts(), theGate(),
|
| 3015 |
orderIsPageOne(), openingHolds(),
|
| 3016 |
nobodyBlocksTheRoad(), gatekeeperIsOnTheRoad(), stepDownTelegraphs(),
|
| 3017 |
-
watchesYouPass(), laneReads(), theGlanceStillWorks()
|
|
|
|
| 3018 |
return { pass: out.every((r) => r.pass), results: out };
|
| 3019 |
}
|
| 3020 |
|
|
@@ -3048,6 +3308,7 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
|
|
| 3048 |
orderIsPageOne, openingHolds,
|
| 3049 |
nobodyBlocksTheRoad, gatekeeperIsOnTheRoad, stepDownTelegraphs,
|
| 3050 |
watchesYouPass, laneReads, theGlanceStillWorks,
|
|
|
|
| 3051 |
all, frameAt, frameAtX };
|
| 3052 |
|
| 3053 |
return Object.fromEntries(Object.entries(api).map(([k, fn]) => [k, silent(fn)]));
|
|
|
|
| 1583 |
const carriers = armed();
|
| 1584 |
const owner = carriers.find((d) => d.kept === 0); // the forge's
|
| 1585 |
|
| 1586 |
+
// 1. THE WRONG FRAME WAKES THEM. This was "the wrong frame does nothing",
|
| 1587 |
+
// and that is exactly what made the walk home an answer key: pin, see
|
| 1588 |
+
// who walks, try the next door, never read a word. Now a stranger's
|
| 1589 |
+
// house wakes the owner and it comes for him. The page stays on the
|
| 1590 |
+
// wall, and nothing opens or settles.
|
| 1591 |
atDoor(1, 0);
|
| 1592 |
tap({ sheaf: true });
|
| 1593 |
+
const seen = [owner.state];
|
| 1594 |
+
for (let i = 0; i < 60 * 3; i++) {
|
| 1595 |
+
scene.update(1 / 60, NONE);
|
| 1596 |
+
if (seen[seen.length - 1] !== owner.state) seen.push(owner.state);
|
| 1597 |
+
}
|
| 1598 |
const wrong = {
|
| 1599 |
onTheWall: s.houses[1].filed === 0,
|
| 1600 |
+
rose: seen.includes('RISING'),
|
| 1601 |
+
cameForHim: owner.state === 'RUSH' && !owner.homing,
|
| 1602 |
+
states: seen.join(' > '),
|
| 1603 |
nothingOpened: s.houses.every((h) => !h.open),
|
| 1604 |
nothingSettled: s.houses.every((h) => !h.settled),
|
| 1605 |
};
|
| 1606 |
+
// Put it back down before it reaches him. What follows is about the walk
|
| 1607 |
+
// home, not a fight; wrongDoorWakes owns the fight.
|
| 1608 |
+
owner.hits = 99;
|
| 1609 |
+
owner.takeHit(owner.x - 1);
|
| 1610 |
+
drive(1.0);
|
| 1611 |
|
| 1612 |
// 2. AND IT COMES BACK OUT. Brute force is allowed: four fragments, five
|
| 1613 |
// frames, and nothing stops a player working through them.
|
|
|
|
| 1619 |
};
|
| 1620 |
|
| 1621 |
// 3. THE RIGHT FRAME AND HE GETS UP. Nothing is said, here or anywhere.
|
| 1622 |
+
// From wherever it went down last β which, after step 1, is not where
|
| 1623 |
+
// it spawned.
|
| 1624 |
atDoor(0, 0);
|
| 1625 |
+
const from = owner.x;
|
| 1626 |
+
const pinnedAt = s.clock;
|
| 1627 |
tap({ sheaf: true });
|
| 1628 |
const filed = {
|
| 1629 |
onTheWall: s.houses[0].filed === 0,
|
|
|
|
| 1643 |
// 5. IT ARRIVES, AND ONLY ITS OWN DOOR OPENS.
|
| 1644 |
let ticks = 0;
|
| 1645 |
while (owner.state !== 'HOME' && ticks < 60 * 200) { scene.update(1 / 60, NONE); ticks++; }
|
| 1646 |
+
const walked = Math.abs(s.houses[0].x - from);
|
| 1647 |
const arrived = {
|
| 1648 |
home: owner.state === 'HOME',
|
| 1649 |
atTheDoor: Math.round(owner.x) === s.houses[0].x,
|
| 1650 |
doorsOpen: s.houses.filter((h) => h.open).length,
|
| 1651 |
+
// From the pin, not from here: steps 4's second and a half is part of
|
| 1652 |
+
// the walk, and counting from after it hid behind the old error in
|
| 1653 |
+
// where the walk started.
|
| 1654 |
+
seconds: +(s.clock - pinnedAt).toFixed(1),
|
| 1655 |
px: Math.round(walked),
|
| 1656 |
// Derived from HOME_SPEED rather than written down, plus the rise.
|
| 1657 |
expected: +(walked / TUNING.HOME_SPEED + TUNING.RISE_MS / 1000).toFixed(1),
|
|
|
|
| 1666 |
|
| 1667 |
return {
|
| 1668 |
name: 'walks home',
|
| 1669 |
+
pass: wrong.onTheWall && wrong.rose && wrong.cameForHim && wrong.nothingOpened &&
|
| 1670 |
wrong.nothingSettled &&
|
| 1671 |
pulledBack.frameEmpty && pulledBack.stillInTheSheaf &&
|
| 1672 |
filed.onTheWall && filed.settled && filed.gotUp &&
|
|
|
|
| 1675 |
arrived.home && arrived.atTheDoor && arrived.doorsOpen === 1 &&
|
| 1676 |
Math.abs(arrived.seconds - arrived.expected) < 1.5 &&
|
| 1677 |
frozen.ignoredTheSword && frozen.didNotMove && frozen.stillHome,
|
| 1678 |
+
theWrongFrameWakesThem: wrong,
|
| 1679 |
andItComesBackOut: pulledBack,
|
| 1680 |
theRightFrameSendsThemHome: filed,
|
| 1681 |
andThenTheFrameIsFinishedWith: settled,
|
|
|
|
| 2064 |
at(0); mark();
|
| 2065 |
const mapCannotBeFiled = s.sheaf.index < 0 || filed().join(',') === before2;
|
| 2066 |
|
| 2067 |
+
// 7. NO VERDICT, EVER. The world answers every filing now β a person
|
| 2068 |
+
// walking home, or a person coming for you β and that is all it does.
|
| 2069 |
+
// Nothing in the state says right or wrong: no tick, no score, no
|
| 2070 |
+
// `correct` or `wrong` field anywhere. An empty plot is not flagged.
|
| 2071 |
+
// Four filed and one empty is the finding, and it has to look exactly
|
| 2072 |
+
// like four filed and one not done yet.
|
| 2073 |
const noVerdict = !('correct' in s.houses[0]) && !('wrong' in s.houses[0]) &&
|
| 2074 |
s.houses.every((h) => h.filed == null || typeof h.filed === 'number');
|
| 2075 |
|
|
|
|
| 2480 |
const highest = rows.length ? Math.min(...rows.map((r) => r.phoneTop)) : 0;
|
| 2481 |
return {
|
| 2482 |
name: 'band fits',
|
| 2483 |
+
// ELEVEN, EXACTLY. Nine village strings, his orders, and the note on the
|
| 2484 |
+
// tree. An exact equality rather than a floor, so adding a string and not
|
| 2485 |
+
// looking at it fails here rather than on somebody's phone.
|
| 2486 |
+
pass: rows.length === 11 && worst <= BAND.MAX_LINES && highest >= BAND.TOP_FLOOR,
|
| 2487 |
strings: rows.length, longest: worst, allowed: BAND.MAX_LINES,
|
| 2488 |
// How far up the scene the tallest band climbs on the narrowest display,
|
| 2489 |
// and how far up it is allowed to. Bigger number is lower on screen.
|
|
|
|
| 2644 |
};
|
| 2645 |
}
|
| 2646 |
|
| 2647 |
+
// --- wrong doors ----------------------------------------------------------------
|
| 2648 |
+
//
|
| 2649 |
+
// Played through, the level was finished without reading a word: pin a page,
|
| 2650 |
+
// see who walks, try the next door. The walk home was an answer key. So a
|
| 2651 |
+
// stranger's house now wakes the owner, and it comes for him β guessing is
|
| 2652 |
+
// still allowed, and it costs blood. See docs/specs/astara-wrong-door-addendum.md.
|
| 2653 |
+
|
| 2654 |
+
/** States a body passes through over the next few seconds, in order. */
|
| 2655 |
+
const statesOver = (d, secs) => {
|
| 2656 |
+
const seen = [d.state];
|
| 2657 |
+
for (let i = 0; i < Math.round(secs * 60); i++) {
|
| 2658 |
+
scene.update(1 / 60, NONE);
|
| 2659 |
+
if (seen[seen.length - 1] !== d.state) seen.push(d.state);
|
| 2660 |
+
}
|
| 2661 |
+
return seen;
|
| 2662 |
+
};
|
| 2663 |
+
/** Put a body down on the spot. The fixture armed() already uses. */
|
| 2664 |
+
const putDown = (d) => { d.hits = 99; d.takeHit(d.x - 1); drive(1.0); };
|
| 2665 |
+
|
| 2666 |
+
/**
|
| 2667 |
+
* A WRONG DOOR WAKES THEM.
|
| 2668 |
+
*
|
| 2669 |
+
* Every carrier, the gatekeeper included: pinned on a door that is not
|
| 2670 |
+
* theirs, the owner rises and comes β RISING, then RUSH, never homing β and
|
| 2671 |
+
* the page stays on the wrong wall. Knocked down, it can be woken again by
|
| 2672 |
+
* the next wrong pin, as often as he makes one. Pinned right, it goes home.
|
| 2673 |
+
*
|
| 2674 |
+
* AND THE BEAT THE RULES MAKE WITHOUT ANYONE DESIGNING IT. `sendHome` from
|
| 2675 |
+
* RUSH goes straight to HOMING, so the way to stop someone coming for you is
|
| 2676 |
+
* to put their words where they belong. A woken body chasing him from beyond
|
| 2677 |
+
* its own door: he pins it right, and it walks up to its door, stops short of
|
| 2678 |
+
* him, turns its back and never touches him. Asserted, because it falls out
|
| 2679 |
+
* of two methods sitting side by side and nothing else protects it.
|
| 2680 |
+
*/
|
| 2681 |
+
function wrongDoorWakes() {
|
| 2682 |
+
const rows = [];
|
| 2683 |
+
for (const c of armed()) {
|
| 2684 |
+
const k = c.kept;
|
| 2685 |
+
// A door that is not theirs AND not finished with. A door filed right is
|
| 2686 |
+
// locked, and the rows before this one have filed theirs right.
|
| 2687 |
+
const w = s.houses.map((_, i) => (k + 1 + i) % s.houses.length)
|
| 2688 |
+
.find((i) => i !== k && !s.houses[i].settled);
|
| 2689 |
+
atDoor(w, k);
|
| 2690 |
+
tap({ sheaf: true });
|
| 2691 |
+
const first = statesOver(c, 2.5);
|
| 2692 |
+
const woke = first.includes('RISING') && c.state === 'RUSH' && !c.homing && c.revealed;
|
| 2693 |
+
const leftUp = s.houses[w].filed === k;
|
| 2694 |
+
putDown(c);
|
| 2695 |
+
const downed = c.state === 'DOWNED';
|
| 2696 |
+
const stillUp = s.houses[w].filed === k;
|
| 2697 |
+
atDoor(w, k);
|
| 2698 |
+
tap({ sheaf: true }); // take it back down...
|
| 2699 |
+
const retrieved = s.houses[w].filed == null;
|
| 2700 |
+
atDoor(w, k);
|
| 2701 |
+
tap({ sheaf: true }); // ...and pin it wrong again
|
| 2702 |
+
const again = statesOver(c, 2.5);
|
| 2703 |
+
const wokeAgain = again.includes('RISING') && c.state === 'RUSH' && !c.homing;
|
| 2704 |
+
putDown(c);
|
| 2705 |
+
atDoor(w, k);
|
| 2706 |
+
tap({ sheaf: true }); // off the wrong wall
|
| 2707 |
+
atDoor(k, k);
|
| 2708 |
+
tap({ sheaf: true }); // onto its own
|
| 2709 |
+
const home = c.homing && (c.state === 'RISING' || c.state === 'HOMING') &&
|
| 2710 |
+
c.homeX === s.houses[k].x;
|
| 2711 |
+
rows.push({ kept: k, wrongDoor: w, woke, pageStaysUp: leftUp && stillUp, downed,
|
| 2712 |
+
retrieved, wokeAgain, goesHomeWhenRight: home, states: first.join(' > ') });
|
| 2713 |
+
}
|
| 2714 |
+
|
| 2715 |
+
// The beat. A carrier that fell beyond its own door, from where he pins,
|
| 2716 |
+
// and a wrong door on his side of it β so it chases him past nothing, and
|
| 2717 |
+
// its own door is between them when he puts the page up.
|
| 2718 |
+
const beat = (() => {
|
| 2719 |
+
for (const seed of [7, 11, 23, 41, 97, 131]) {
|
| 2720 |
+
for (const c of armed(seed).filter((d) => !d.post)) {
|
| 2721 |
+
const k = c.kept;
|
| 2722 |
+
const door = s.houses[k].x;
|
| 2723 |
+
if (!(c.x > door + 300)) continue;
|
| 2724 |
+
const w = s.houses.findIndex((h, i) => i !== k && h.x < door - 300);
|
| 2725 |
+
if (w < 0) continue;
|
| 2726 |
+
atDoor(w, k);
|
| 2727 |
+
tap({ sheaf: true }); // wrong: it wakes
|
| 2728 |
+
atDoor(w, k);
|
| 2729 |
+
tap({ sheaf: true }); // straight back down
|
| 2730 |
+
// He goes to its door and waits; it comes for him, still beyond it.
|
| 2731 |
+
s.prince.x = s.prince.prevX = door - 300;
|
| 2732 |
+
s.prince.facing = 1;
|
| 2733 |
+
s.camera.reset(s.prince.x);
|
| 2734 |
+
let t = 0;
|
| 2735 |
+
while (!(c.state === 'RUSH' && c.x - s.prince.x < W / 2) && t < 60 * 60) {
|
| 2736 |
+
scene.update(1 / 60, NONE);
|
| 2737 |
+
t++;
|
| 2738 |
+
}
|
| 2739 |
+
const closing = c.state === 'RUSH' && c.x - s.prince.x < W / 2 && c.x > door;
|
| 2740 |
+
const cameAtHim = c.facing === -1;
|
| 2741 |
+
const hits0 = s.prince.hits;
|
| 2742 |
+
atDoor(k, k);
|
| 2743 |
+
tap({ sheaf: true }); // right: it stops coming
|
| 2744 |
+
const stopped = c.state === 'HOMING' && c.homing && c.homeX === door;
|
| 2745 |
+
let u = 0, hurt = false, nearest = Infinity;
|
| 2746 |
+
while (c.state !== 'HOME' && u < 60 * 60) {
|
| 2747 |
+
scene.update(1 / 60, NONE);
|
| 2748 |
+
u++;
|
| 2749 |
+
if (s.prince.hits > hits0) hurt = true;
|
| 2750 |
+
nearest = Math.min(nearest, Math.abs(c.x - s.prince.x));
|
| 2751 |
+
}
|
| 2752 |
+
return {
|
| 2753 |
+
seed, kept: k, secondsToClose: +(t / 60).toFixed(1), closing, cameAtHim, stopped,
|
| 2754 |
+
atItsDoor: c.state === 'HOME' && Math.round(c.x) === door,
|
| 2755 |
+
turnedItsBack: c.facing === Math.sign(c.x - s.prince.x),
|
| 2756 |
+
neverTouchedHim: !hurt, closestItCame: Math.round(nearest),
|
| 2757 |
+
};
|
| 2758 |
+
}
|
| 2759 |
+
}
|
| 2760 |
+
return null;
|
| 2761 |
+
})();
|
| 2762 |
+
scene.reset();
|
| 2763 |
+
return {
|
| 2764 |
+
name: 'a wrong door wakes them',
|
| 2765 |
+
pass: rows.length === 4 &&
|
| 2766 |
+
rows.every((r) => r.woke && r.pageStaysUp && r.downed && r.retrieved &&
|
| 2767 |
+
r.wokeAgain && r.goesHomeWhenRight) &&
|
| 2768 |
+
!!beat && beat.closing && beat.cameAtHim && beat.stopped && beat.atItsDoor &&
|
| 2769 |
+
beat.turnedItsBack && beat.neverTouchedHim,
|
| 2770 |
+
rows, theBeat: beat,
|
| 2771 |
+
};
|
| 2772 |
+
}
|
| 2773 |
+
|
| 2774 |
+
/**
|
| 2775 |
+
* THE GATEKEEPER WAKES AT HIS POST.
|
| 2776 |
+
*
|
| 2777 |
+
* He is the first page most players pin, and pinned wrong he comes for them
|
| 2778 |
+
* with the spear β the hardest fight in the level, and the first time a
|
| 2779 |
+
* player learns that where things go matters. But he does not leave the
|
| 2780 |
+
* gate: pinned from the far end of the village he wakes and waits there,
|
| 2781 |
+
* and at the gate he braces, thrusts and shoves, exactly as before.
|
| 2782 |
+
*/
|
| 2783 |
+
function gatekeeperWakesAtHisPost() {
|
| 2784 |
+
armed();
|
| 2785 |
+
const gk = gatekeeper();
|
| 2786 |
+
const post = gk.x;
|
| 2787 |
+
atDoor(0, gk.kept); // the forge: the far end
|
| 2788 |
+
tap({ sheaf: true });
|
| 2789 |
+
const seen = statesOver(gk, 3);
|
| 2790 |
+
const woke = seen.includes('RISING') && gk.state === 'RUSH' && gk.revealed && !gk.homing;
|
| 2791 |
+
const stayedWhileHeWasAway = gk.x === post;
|
| 2792 |
+
s.prince.x = s.prince.prevX = s.gateStopX;
|
| 2793 |
+
s.prince.facing = 1;
|
| 2794 |
+
s.camera.reset(s.prince.x);
|
| 2795 |
+
const hits0 = s.prince.hits;
|
| 2796 |
+
const phases = new Set();
|
| 2797 |
+
let shoved = false, moved = false;
|
| 2798 |
+
for (let i = 0; i < 60 * 4; i++) {
|
| 2799 |
+
const before = s.prince.x;
|
| 2800 |
+
scene.update(1 / 60, NONE);
|
| 2801 |
+
phases.add(gk.phase);
|
| 2802 |
+
if (s.prince.x < before - 60) shoved = true;
|
| 2803 |
+
if (gk.x !== post) moved = true;
|
| 2804 |
+
}
|
| 2805 |
+
const hitsTaken = s.prince.hits - hits0;
|
| 2806 |
+
scene.reset();
|
| 2807 |
+
return {
|
| 2808 |
+
name: 'the gatekeeper wakes at his post',
|
| 2809 |
+
pass: woke && stayedWhileHeWasAway && !moved && phases.size >= 3 && shoved,
|
| 2810 |
+
woke, states: seen.join(' > '), stayedWhileHeWasAway, movedAtTheGate: moved,
|
| 2811 |
+
phasesSeen: [...phases].sort(), shovedHim: shoved, hitsItCostHim: hitsTaken,
|
| 2812 |
+
};
|
| 2813 |
+
}
|
| 2814 |
+
|
| 2815 |
+
/**
|
| 2816 |
+
* THE TREE TEACHES THE RULE, AND NOTHING ELSE.
|
| 2817 |
+
*
|
| 2818 |
+
* A note nailed to a tree on the road: they cannot speak, what they carried
|
| 2819 |
+
* is all that is left of them, take it home. It stands past the first
|
| 2820 |
+
* drained, so "they" has somebody to mean, and before the village, so it is
|
| 2821 |
+
* read before the first fight β and far enough from every drained that
|
| 2822 |
+
* lighting it wakes nobody.
|
| 2823 |
+
*
|
| 2824 |
+
* Light it and the band reads it. Take it and it is page two, the rebels'
|
| 2825 |
+
* instruction after the palace's, carrying a tree at its foot. The nail it
|
| 2826 |
+
* leaves is bare and asks for nothing: holding a page there, nothing is
|
| 2827 |
+
* offered. A restart puts the note back.
|
| 2828 |
+
*/
|
| 2829 |
+
function theTreeTeaches() {
|
| 2830 |
+
scene.reset();
|
| 2831 |
+
const t = s.tree;
|
| 2832 |
+
if (!t) return { name: 'the tree teaches', pass: false, why: 'there is no tree' };
|
| 2833 |
+
const note = s.letters.byHouse[t.index]?.letter ?? null;
|
| 2834 |
+
const road = s.drained.filter((d) => !s.village.includes(d));
|
| 2835 |
+
const first = Math.min(...road.map((d) => d.x));
|
| 2836 |
+
const clearOfAll = Math.min(...road.map((d) => Math.abs(d.x - t.x)));
|
| 2837 |
+
const placed = t.x > first + TUNING.TREE_CLEAR && t.x < BOWL.X0 &&
|
| 2838 |
+
clearOfAll >= TUNING.TREE_CLEAR;
|
| 2839 |
+
|
| 2840 |
+
// Up to it in the dark: nothing read yet.
|
| 2841 |
+
s.prince.x = s.prince.prevX = t.x - 300;
|
| 2842 |
+
s.prince.facing = 1;
|
| 2843 |
+
s.camera.reset(s.prince.x);
|
| 2844 |
+
drive(0.5);
|
| 2845 |
+
const unreadInTheDark = !t.letterSeen && s.band?.text !== note;
|
| 2846 |
+
|
| 2847 |
+
// Lit: the band reads it, and nobody wakes.
|
| 2848 |
+
drive(1.4, { torch: true });
|
| 2849 |
+
const reads = t.letterSeen && s.band?.text === note && s.canTakeLetter;
|
| 2850 |
+
const nobodyWoke = s.drained.every((d) => !d.revealed);
|
| 2851 |
+
|
| 2852 |
+
// Taken: page two, the tree's, after his orders.
|
| 2853 |
+
const before = s.sheaf.pages.length;
|
| 2854 |
+
tap();
|
| 2855 |
+
const p = s.sheaf.pages[1];
|
| 2856 |
+
const pageTwo = before === 1 && s.sheaf.pages.length === 2 && p?.kind === 'letter' &&
|
| 2857 |
+
p.house === t.index && s.textFor(p) === note && s.sheaf.pages[0].house !== t.index;
|
| 2858 |
+
const origin = s.letters.byHouse[p?.house]?.origin;
|
| 2859 |
+
const bare = t.letterTaken && !s.canTakeLetter;
|
| 2860 |
+
|
| 2861 |
+
// Nothing goes back on it: holding a page there, nothing is offered.
|
| 2862 |
+
s.prince.inventory.push({ kept: 0, answers: null, seen: true });
|
| 2863 |
+
s.sheaf.pages.push({ kind: 'fragment', kept: 0 });
|
| 2864 |
+
s.sheaf.index = s.sheaf.pages.length - 1;
|
| 2865 |
+
drive(0.5, { torch: true });
|
| 2866 |
+
drive(0.3);
|
| 2867 |
+
const nothingGoesBack = !s.canPin && !s.canMark && !s.canTakeLetter;
|
| 2868 |
+
|
| 2869 |
+
scene.reset();
|
| 2870 |
+
const backOnItsNail = !t.letterTaken && !t.letterSeen;
|
| 2871 |
+
return {
|
| 2872 |
+
name: 'the tree teaches',
|
| 2873 |
+
pass: !!note && placed && unreadInTheDark && reads && nobodyWoke && pageTwo &&
|
| 2874 |
+
origin === 'tree' && bare && nothingGoesBack && backOnItsNail,
|
| 2875 |
+
at: t.x, pastTheFirstDrained: t.x - first, clearOfEveryDrained: clearOfAll,
|
| 2876 |
+
clearFloor: TUNING.TREE_CLEAR, beforeTheVillage: t.x < BOWL.X0,
|
| 2877 |
+
unreadInTheDark, readsUnderTheTorch: reads, nobodyWoke,
|
| 2878 |
+
pageTwo, originOnThePage: origin, bareNailAfter: bare, nothingGoesBack, backOnItsNail,
|
| 2879 |
+
};
|
| 2880 |
+
}
|
| 2881 |
+
|
| 2882 |
// --- the two lanes ------------------------------------------------------------
|
| 2883 |
//
|
| 2884 |
// Every player who saw a Drained on his road fought it: no side scroller lets
|
|
|
|
| 3273 |
nothingOutOfReach(), healthCosts(), theGate(),
|
| 3274 |
orderIsPageOne(), openingHolds(),
|
| 3275 |
nobodyBlocksTheRoad(), gatekeeperIsOnTheRoad(), stepDownTelegraphs(),
|
| 3276 |
+
watchesYouPass(), laneReads(), theGlanceStillWorks(),
|
| 3277 |
+
wrongDoorWakes(), gatekeeperWakesAtHisPost(), theTreeTeaches()];
|
| 3278 |
return { pass: out.every((r) => r.pass), results: out };
|
| 3279 |
}
|
| 3280 |
|
|
|
|
| 3308 |
orderIsPageOne, openingHolds,
|
| 3309 |
nobodyBlocksTheRoad, gatekeeperIsOnTheRoad, stepDownTelegraphs,
|
| 3310 |
watchesYouPass, laneReads, theGlanceStillWorks,
|
| 3311 |
+
wrongDoorWakes, gatekeeperWakesAtHisPost, theTreeTeaches,
|
| 3312 |
all, frameAt, frameAtX };
|
| 3313 |
|
| 3314 |
return Object.fromEntries(Object.entries(api).map(([k, fn]) => [k, silent(fn)]));
|
src/drained.js
CHANGED
|
@@ -16,6 +16,8 @@
|
|
| 16 |
// STAGGER knocked back, cannot act.
|
| 17 |
// DOWNED lies where it fell. Cannot be revealed. Cannot rush.
|
| 18 |
// RISING getting up. Can be knocked back down, for ever.
|
|
|
|
|
|
|
| 19 |
// HOMING walking back to its own door, because somebody gave it its name.
|
| 20 |
// Harmless, never hurries, and the only thing in this game that
|
| 21 |
// goes somewhere on purpose.
|
|
@@ -223,6 +225,11 @@ export function createDrained(clips, spawnX0, dir, keptIndex = 0, opts = {}) {
|
|
| 223 |
// rush, and it is the one flag in here that the player's own act sets.
|
| 224 |
homing: false,
|
| 225 |
homeX: null,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
};
|
| 227 |
|
| 228 |
/**
|
|
@@ -294,6 +301,8 @@ export function createDrained(clips, spawnX0, dir, keptIndex = 0, opts = {}) {
|
|
| 294 |
// moves it, so a state that is not these two is simply on the road.
|
| 295 |
d.lane = (next === WANDER || next === STEP_DOWN) && !post ? 1 : 0;
|
| 296 |
if (next !== WANDER) d.watching = false;
|
|
|
|
|
|
|
| 297 |
players[next].reset();
|
| 298 |
applyPose();
|
| 299 |
}
|
|
@@ -336,6 +345,7 @@ export function createDrained(clips, spawnX0, dir, keptIndex = 0, opts = {}) {
|
|
| 336 |
d.knockVel = 0;
|
| 337 |
d.homing = false;
|
| 338 |
d.homeX = null;
|
|
|
|
| 339 |
d.watching = false;
|
| 340 |
d.stir = 0;
|
| 341 |
// THE GATEKEEPER'S CYCLE, and it was surviving a reset.
|
|
@@ -368,9 +378,37 @@ export function createDrained(clips, spawnX0, dir, keptIndex = 0, opts = {}) {
|
|
| 368 |
if (d.state === HOMING || d.state === HOME) return;
|
| 369 |
d.homeX = x;
|
| 370 |
d.homing = true;
|
|
|
|
| 371 |
enter(d.state === DOWNED ? RISING : HOMING);
|
| 372 |
};
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
/** A connected sword hit. Returns true if it did anything at all. */
|
| 375 |
d.takeHit = (fromX) => {
|
| 376 |
// Hitting a DOWNED one does nothing. No knockback, no reaction, no counter.
|
|
@@ -553,7 +591,10 @@ export function createDrained(clips, spawnX0, dir, keptIndex = 0, opts = {}) {
|
|
| 553 |
case RUSH: {
|
| 554 |
const toward = Math.sign(ctx.princeX - d.x) || -1;
|
| 555 |
d.facing = toward;
|
| 556 |
-
|
|
|
|
|
|
|
|
|
|
| 557 |
if (postClip) {
|
| 558 |
// HE DOES NOT CLOSE THE DISTANCE. Sixty years after anyone relieved
|
| 559 |
// him, he is still keeping people away from a gate β so he stands
|
|
|
|
| 16 |
// STAGGER knocked back, cannot act.
|
| 17 |
// DOWNED lies where it fell. Cannot be revealed. Cannot rush.
|
| 18 |
// RISING getting up. Can be knocked back down, for ever.
|
| 19 |
+
// A body woken by a WRONG pin rises into RUSH too, and comes for him
|
| 20 |
+
// from wherever it fell β off screen included. See wake().
|
| 21 |
// HOMING walking back to its own door, because somebody gave it its name.
|
| 22 |
// Harmless, never hurries, and the only thing in this game that
|
| 23 |
// goes somewhere on purpose.
|
|
|
|
| 225 |
// rush, and it is the one flag in here that the player's own act sets.
|
| 226 |
homing: false,
|
| 227 |
homeX: null,
|
| 228 |
+
// WOKEN BY A WRONG PIN. Someone put its last words on a stranger's house,
|
| 229 |
+
// and it is coming for him β from wherever it fell, so this is also what
|
| 230 |
+
// keeps it out of the distance cull and moving while he cannot see it.
|
| 231 |
+
// Cleared the moment it goes down again or is sent home.
|
| 232 |
+
woken: false,
|
| 233 |
};
|
| 234 |
|
| 235 |
/**
|
|
|
|
| 301 |
// moves it, so a state that is not these two is simply on the road.
|
| 302 |
d.lane = (next === WANDER || next === STEP_DOWN) && !post ? 1 : 0;
|
| 303 |
if (next !== WANDER) d.watching = false;
|
| 304 |
+
// Only rising, rushing and reeling are part of coming for him.
|
| 305 |
+
if (next !== RISING && next !== RUSH && next !== STAGGER) d.woken = false;
|
| 306 |
players[next].reset();
|
| 307 |
applyPose();
|
| 308 |
}
|
|
|
|
| 345 |
d.knockVel = 0;
|
| 346 |
d.homing = false;
|
| 347 |
d.homeX = null;
|
| 348 |
+
d.woken = false;
|
| 349 |
d.watching = false;
|
| 350 |
d.stir = 0;
|
| 351 |
// THE GATEKEEPER'S CYCLE, and it was surviving a reset.
|
|
|
|
| 378 |
if (d.state === HOMING || d.state === HOME) return;
|
| 379 |
d.homeX = x;
|
| 380 |
d.homing = true;
|
| 381 |
+
d.woken = false;
|
| 382 |
enter(d.state === DOWNED ? RISING : HOMING);
|
| 383 |
};
|
| 384 |
|
| 385 |
+
/**
|
| 386 |
+
* Somebody put this one's words on the WRONG wall, so it gets up and comes
|
| 387 |
+
* for him. The same rise as going home; RISING already forks on `homing`,
|
| 388 |
+
* so this is that flag held false and the rise started.
|
| 389 |
+
*
|
| 390 |
+
* A NORMAL REVEALED DRAINED from here: RUSH, three hits to put down. It is
|
| 391 |
+
* already on the road where it fell, so there is no step down. `revealed`
|
| 392 |
+
* is set because the gatekeeper β and anything downed without a look β was
|
| 393 |
+
* never revealed, and the guard pose and the fight count read that flag.
|
| 394 |
+
*
|
| 395 |
+
* Returns whether it actually woke. One already up is left as it is: it is
|
| 396 |
+
* coming anyway. One going home, or home, was filed right and is finished.
|
| 397 |
+
*/
|
| 398 |
+
d.wake = () => {
|
| 399 |
+
if (d.state === HOMING || d.state === HOME) return false;
|
| 400 |
+
d.homing = false;
|
| 401 |
+
d.homeX = null;
|
| 402 |
+
d.revealed = true;
|
| 403 |
+
if (d.state === DOWNED) {
|
| 404 |
+
enter(RISING);
|
| 405 |
+
d.woken = true;
|
| 406 |
+
return true;
|
| 407 |
+
}
|
| 408 |
+
if (d.state === RISING) { d.woken = true; return true; }
|
| 409 |
+
return false;
|
| 410 |
+
};
|
| 411 |
+
|
| 412 |
/** A connected sword hit. Returns true if it did anything at all. */
|
| 413 |
d.takeHit = (fromX) => {
|
| 414 |
// Hitting a DOWNED one does nothing. No knockback, no reaction, no counter.
|
|
|
|
| 591 |
case RUSH: {
|
| 592 |
const toward = Math.sign(ctx.princeX - d.x) || -1;
|
| 593 |
d.facing = toward;
|
| 594 |
+
// Off screen it waits β unless a wrong pin woke it, in which case it is
|
| 595 |
+
// coming for him and does not need to see him to do it. The gatekeeper
|
| 596 |
+
// waits either way: he is angry at the gate, not chasing anyone.
|
| 597 |
+
if (!ctx.inFrame && (!d.woken || postClip)) break;
|
| 598 |
if (postClip) {
|
| 599 |
// HE DOES NOT CLOSE THE DISTANCE. Sixty years after anyone relieved
|
| 600 |
// him, he is still keeping people away from a gate β so he stands
|
src/render.js
CHANGED
|
@@ -260,6 +260,37 @@ export function createRenderer(ctx) {
|
|
| 260 |
}
|
| 261 |
}
|
| 262 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
/**
|
| 264 |
* One Drained, in whichever lane it is standing in.
|
| 265 |
*
|
|
@@ -924,11 +955,13 @@ export function createRenderer(ctx) {
|
|
| 924 |
function drawLetterHouse(c, s, sheet, x0, y0, pg) {
|
| 925 |
if (!pg || pg.kind !== 'letter') return;
|
| 926 |
const house = s.houses?.[pg.house];
|
| 927 |
-
// WHERE IT CAME FROM. Five of the
|
| 928 |
-
//
|
| 929 |
-
//
|
| 930 |
-
//
|
| 931 |
-
const
|
|
|
|
|
|
|
| 932 |
if (!hc) return;
|
| 933 |
const frame = house ? house.variant : 0;
|
| 934 |
const bx = x0 + sheet.width / 2;
|
|
@@ -1147,7 +1180,7 @@ export function createRenderer(ctx) {
|
|
| 1147 |
* LIT PASS ONLY, so all three live and die with the torch like everything
|
| 1148 |
* else in a doorway.
|
| 1149 |
*/
|
| 1150 |
-
function drawNail(c, s, clip, house, x, y, clock) {
|
| 1151 |
if (!clip.nailAt) return;
|
| 1152 |
|
| 1153 |
// 1. THE LETTER, while it is still hanging there.
|
|
@@ -1193,7 +1226,10 @@ export function createRenderer(ctx) {
|
|
| 1193 |
// question, which is the same reason four empty sockets on the gate have
|
| 1194 |
// never needed explaining β and it is why the loom house goes on asking
|
| 1195 |
// for the whole level, with nobody left to answer it.
|
| 1196 |
-
|
|
|
|
|
|
|
|
|
|
| 1197 |
const pulse = 1 - TORCH.KEPT_PULSE_DEPTH *
|
| 1198 |
(0.5 - 0.5 * Math.cos(clock * Math.PI * 2 * TORCH.KEPT_PULSE_HZ));
|
| 1199 |
const cx = px0 + nw / 2;
|
|
@@ -1255,7 +1291,7 @@ export function createRenderer(ctx) {
|
|
| 1255 |
// AND IT IS NOT A VERDICT. It appears for the wrong page just as readily,
|
| 1256 |
// because it is a preview of an act and not an opinion about it.
|
| 1257 |
if (house.filed == null) {
|
| 1258 |
-
if (!s.canPin || s.markHouse !== house.index) return;
|
| 1259 |
const held = s.handPage;
|
| 1260 |
const ghostArt = held?.kind === 'fragment' ? s.kept : null;
|
| 1261 |
const gf = ghostArt?.frames[held.kept];
|
|
@@ -1606,6 +1642,7 @@ export function createRenderer(ctx) {
|
|
| 1606 |
drawGround(ctx, s.ground, 'dark', 0, W, camX);
|
| 1607 |
drawBolt(ctx, s);
|
| 1608 |
drawVillage(ctx, s, camX, 'dark');
|
|
|
|
| 1609 |
drawFlash(ctx, s.flash ?? 0);
|
| 1610 |
drawAllDrained(ctx, s, 'dark', camX, alpha);
|
| 1611 |
drawClip(ctx, s.prince.clip, s.prince.frame, 'dark', px, GROUND_Y + limpDip, camX, face, s.prince.eyeDim);
|
|
@@ -1648,6 +1685,7 @@ export function createRenderer(ctx) {
|
|
| 1648 |
// And here the doors open. This is the one thing in the level the
|
| 1649 |
// torch reveals that is not a person.
|
| 1650 |
drawVillage(octx, s, camX, 'lit');
|
|
|
|
| 1651 |
// He is never lit by his own torch, so he goes in with his DARK art β
|
| 1652 |
// the same trick the far layers use.
|
| 1653 |
// The Drained are the thing the torch is for.
|
|
|
|
| 260 |
}
|
| 261 |
}
|
| 262 |
|
| 263 |
+
/**
|
| 264 |
+
* The tree with the note on it. On the road at his own scale, so it draws
|
| 265 |
+
* with the houses: black in the dark pass, its bark in the lit one.
|
| 266 |
+
*
|
| 267 |
+
* THE NOTE, BOTH WAYS. In the dark the scrap shows pale β paper catching
|
| 268 |
+
* what light there is, so a lone tree says "something is pinned here" the
|
| 269 |
+
* way a house's lit doorway does. Under the torch it is the letter on its
|
| 270 |
+
* nail, drawn by drawNail like every other; once taken, a bare nail and a
|
| 271 |
+
* paler patch, and β `quiet` β nothing asking to be filled.
|
| 272 |
+
*/
|
| 273 |
+
function drawTree(c, s, camX, mode) {
|
| 274 |
+
const t = s.tree;
|
| 275 |
+
const clip = s.treeClip;
|
| 276 |
+
if (!t || !clip) return;
|
| 277 |
+
const sx = Math.round(t.x - camX);
|
| 278 |
+
if (sx < -clip.width || sx > W + clip.width) return;
|
| 279 |
+
const x = sx - clip.pivotX;
|
| 280 |
+
const y = GROUND_Y - clip.pivotY;
|
| 281 |
+
c.drawImage(clip.frames[0][mode === 'lit' ? 'lit' : 'dark'], x, y);
|
| 282 |
+
if (mode === 'lit') {
|
| 283 |
+
drawNail(c, s, clip, t, x, y, s.clock ?? 0, true);
|
| 284 |
+
} else if (!t.letterTaken && s.treeNoteDark) {
|
| 285 |
+
const art = s.letterArt;
|
| 286 |
+
const b = nailBox(clip, t, x, y, art);
|
| 287 |
+
if (b) {
|
| 288 |
+
c.drawImage(s.treeNoteDark, b.px - b.ink.x * b.k, b.py - b.ink.y * b.k,
|
| 289 |
+
art.width * b.k, art.height * b.k);
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
/**
|
| 295 |
* One Drained, in whichever lane it is standing in.
|
| 296 |
*
|
|
|
|
| 955 |
function drawLetterHouse(c, s, sheet, x0, y0, pg) {
|
| 956 |
if (!pg || pg.kind !== 'letter') return;
|
| 957 |
const house = s.houses?.[pg.house];
|
| 958 |
+
// WHERE IT CAME FROM. Five of the letters come from a house in this village
|
| 959 |
+
// and show it. The order came from the palace and the note from a tree on
|
| 960 |
+
// the road, and neither has an entry in `houses` β so their own `origin`
|
| 961 |
+
// says which, and no house and no origin still means the palace.
|
| 962 |
+
const origin = house ? null : s.letters?.byHouse?.[pg.house]?.origin;
|
| 963 |
+
const hc = house ? s.houseClip
|
| 964 |
+
: origin === 'tree' ? (s.treeIconClip ?? s.palaceClip) : s.palaceClip;
|
| 965 |
if (!hc) return;
|
| 966 |
const frame = house ? house.variant : 0;
|
| 967 |
const bx = x0 + sheet.width / 2;
|
|
|
|
| 1180 |
* LIT PASS ONLY, so all three live and die with the torch like everything
|
| 1181 |
* else in a doorway.
|
| 1182 |
*/
|
| 1183 |
+
function drawNail(c, s, clip, house, x, y, clock, quiet = false) {
|
| 1184 |
if (!clip.nailAt) return;
|
| 1185 |
|
| 1186 |
// 1. THE LETTER, while it is still hanging there.
|
|
|
|
| 1226 |
// question, which is the same reason four empty sockets on the gate have
|
| 1227 |
// never needed explaining β and it is why the loom house goes on asking
|
| 1228 |
// for the whole level, with nobody left to answer it.
|
| 1229 |
+
//
|
| 1230 |
+
// NOT ON THE TREE (`quiet`). Nothing ever goes back on it, so its empty
|
| 1231 |
+
// patch must not ask for anything: a bare nail and a paler mark, no more.
|
| 1232 |
+
if (house.filed == null && !quiet) {
|
| 1233 |
const pulse = 1 - TORCH.KEPT_PULSE_DEPTH *
|
| 1234 |
(0.5 - 0.5 * Math.cos(clock * Math.PI * 2 * TORCH.KEPT_PULSE_HZ));
|
| 1235 |
const cx = px0 + nw / 2;
|
|
|
|
| 1291 |
// AND IT IS NOT A VERDICT. It appears for the wrong page just as readily,
|
| 1292 |
// because it is a preview of an act and not an opinion about it.
|
| 1293 |
if (house.filed == null) {
|
| 1294 |
+
if (quiet || !s.canPin || s.markHouse !== house.index) return;
|
| 1295 |
const held = s.handPage;
|
| 1296 |
const ghostArt = held?.kind === 'fragment' ? s.kept : null;
|
| 1297 |
const gf = ghostArt?.frames[held.kept];
|
|
|
|
| 1642 |
drawGround(ctx, s.ground, 'dark', 0, W, camX);
|
| 1643 |
drawBolt(ctx, s);
|
| 1644 |
drawVillage(ctx, s, camX, 'dark');
|
| 1645 |
+
drawTree(ctx, s, camX, 'dark');
|
| 1646 |
drawFlash(ctx, s.flash ?? 0);
|
| 1647 |
drawAllDrained(ctx, s, 'dark', camX, alpha);
|
| 1648 |
drawClip(ctx, s.prince.clip, s.prince.frame, 'dark', px, GROUND_Y + limpDip, camX, face, s.prince.eyeDim);
|
|
|
|
| 1685 |
// And here the doors open. This is the one thing in the level the
|
| 1686 |
// torch reveals that is not a person.
|
| 1687 |
drawVillage(octx, s, camX, 'lit');
|
| 1688 |
+
drawTree(octx, s, camX, 'lit');
|
| 1689 |
// He is never lit by his own torch, so he goes in with his DARK art β
|
| 1690 |
// the same trick the far layers use.
|
| 1691 |
// The Drained are the thing the torch is for.
|
src/scene_level.js
CHANGED
|
@@ -151,6 +151,9 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 151 |
// sky behind the action. A silhouette only shows up against light, so this is
|
| 152 |
// the one thing Blade Warrior does that costs nothing β the fight belongs
|
| 153 |
// under the bright patch. See background.skyOpenAt for what "open" counts.
|
|
|
|
|
|
|
|
|
|
| 154 |
{
|
| 155 |
const L = TUNING.LEVEL_LENGTH_PX;
|
| 156 |
const n = TUNING.ROAD_ENCOUNTERS;
|
|
@@ -169,6 +172,8 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 169 |
// is 680 across β so one raise woke both, on the stretch that is
|
| 170 |
// supposed to be teaching him what raising it costs.
|
| 171 |
if (drained.some((o) => Math.abs(o.x - cand) < TORCH.RADIUS * 2)) continue;
|
|
|
|
|
|
|
| 172 |
// Gentle pull to the middle of the window so ties do not drift to an end.
|
| 173 |
const score = background.skyOpenAt(cand, W) - Math.abs(cand - centre) * 0.6;
|
| 174 |
if (score > best) { best = score; bestX = Math.round(cand); }
|
|
@@ -386,6 +391,33 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 386 |
const socketArt = clips.key
|
| 387 |
? (await tintFrames(clips.key, PALETTE.WATCHERS, 'dark'))[0] : null;
|
| 388 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
// --- the twelve --------------------------------------------------------
|
| 390 |
// Twelve shapes you can swing at, against the background watchers you cannot.
|
| 391 |
// Two populations doing different jobs: the watchers make it feel like a
|
|
@@ -880,6 +912,7 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 880 |
seed: SEED,
|
| 881 |
houses: HOUSES, houseTint: PALETTE.WATCHERS,
|
| 882 |
houseClip, palaceClip, houseArt, doorPoint, keyPoint, letterPoint,
|
|
|
|
| 883 |
village, gateX: GATE_X, gateStopX: GATE_STOP_X,
|
| 884 |
villageGap: VILLAGE_GAP, bowlX0: BOWL.X0,
|
| 885 |
flame: { clip: clips.fire, player: createPlayer(clips.fire), frame: 0 },
|
|
@@ -912,6 +945,7 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 912 |
h.letterSeen = false; h.letterTaken = false;
|
| 913 |
h.open = false; h.settled = false; h.keyTaken = false;
|
| 914 |
}
|
|
|
|
| 915 |
prevPickup = false;
|
| 916 |
prince.attacking = false;
|
| 917 |
prince.attackTime = 0;
|
|
@@ -1132,7 +1166,10 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 1132 |
// β filing correctly from the far side of the village put a body on its
|
| 1133 |
// knees for ever. The flag covers the whole journey; it is cleared when
|
| 1134 |
// the door opens, and the cull takes over again.
|
| 1135 |
-
|
|
|
|
|
|
|
|
|
|
| 1136 |
// On screen together? The camera follows the prince, so this is the same
|
| 1137 |
// question as "can it see him".
|
| 1138 |
const sx = d.x - camera.x;
|
|
@@ -1185,8 +1222,19 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 1185 |
// They are BODY sounds and never tool sounds, and audio.js says at
|
| 1186 |
// length why: a hammer-on-anvil clang would send every player to the
|
| 1187 |
// swinging shape, and the forge's fragment is deliberately on a hauler.
|
| 1188 |
-
|
| 1189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1190 |
// THE INSTANT IT NOTICES HIM. `revealed` latches and never unlatches, so
|
| 1191 |
// this fires once per shape per run β about twelve times, and every one
|
| 1192 |
// of them is a fight he chose by deciding to see.
|
|
@@ -1300,6 +1348,11 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 1300 |
h.letterSeen = true;
|
| 1301 |
}
|
| 1302 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1303 |
|
| 1304 |
// TAKING IT DOES NOT NEED THE TORCH STILL UP. Seeing latches; taking is a
|
| 1305 |
// separate act, exactly as it is for a page on the ground, which he can
|
|
@@ -1314,6 +1367,12 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 1314 |
const p = letterPoint(h);
|
| 1315 |
if (p && Math.abs(prince.x - p[0]) <= TUNING.DOOR_REACH) { litLetter = h; break; }
|
| 1316 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1317 |
|
| 1318 |
// AND THE KEY, WHICH NEEDS THE DOOR OPEN AND NOT THE TORCH UP.
|
| 1319 |
//
|
|
@@ -1525,6 +1584,24 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 1525 |
flash.ms = 0;
|
| 1526 |
flash.thundered = false;
|
| 1527 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1528 |
}
|
| 1529 |
}
|
| 1530 |
}
|
|
|
|
| 151 |
// sky behind the action. A silhouette only shows up against light, so this is
|
| 152 |
// the one thing Blade Warrior does that costs nothing β the fight belongs
|
| 153 |
// under the bright patch. See background.skyOpenAt for what "open" counts.
|
| 154 |
+
// THE TREE GOES DOWN FIRST, the way the doors do, and the three road drained
|
| 155 |
+
// go in what is left. See TREE_CLEAR.
|
| 156 |
+
const TREE_X = Math.round(TUNING.LEVEL_LENGTH_PX * TUNING.TREE_AT);
|
| 157 |
{
|
| 158 |
const L = TUNING.LEVEL_LENGTH_PX;
|
| 159 |
const n = TUNING.ROAD_ENCOUNTERS;
|
|
|
|
| 172 |
// is 680 across β so one raise woke both, on the stretch that is
|
| 173 |
// supposed to be teaching him what raising it costs.
|
| 174 |
if (drained.some((o) => Math.abs(o.x - cand) < TORCH.RADIUS * 2)) continue;
|
| 175 |
+
// And never within the torch's reach of the note on the tree.
|
| 176 |
+
if (Math.abs(cand - TREE_X) < TUNING.TREE_CLEAR) continue;
|
| 177 |
// Gentle pull to the middle of the window so ties do not drift to an end.
|
| 178 |
const score = background.skyOpenAt(cand, W) - Math.abs(cand - centre) * 0.6;
|
| 179 |
if (score > best) { best = score; bestX = Math.round(cand); }
|
|
|
|
| 391 |
const socketArt = clips.key
|
| 392 |
? (await tintFrames(clips.key, PALETTE.WATCHERS, 'dark'))[0] : null;
|
| 393 |
|
| 394 |
+
// --- the tree on the road ------------------------------------------------
|
| 395 |
+
// A rebel nailed the rule to it: they cannot speak, what they carried is all
|
| 396 |
+
// that is left of them, take it home. Never an answer β not which ones, not
|
| 397 |
+
// which home. It is a letter like the houses' in every way the code cares
|
| 398 |
+
// about: light it, read it in the band, take it, and it is a page. It just
|
| 399 |
+
// is not a door, so nothing is ever pinned back to it.
|
| 400 |
+
const treeClip = clips.road_tree ?? null;
|
| 401 |
+
const TREE_NOTE = letters?.byHouse?.findIndex((h) => h.origin === 'tree') ?? -1;
|
| 402 |
+
const TREE = treeClip && TREE_NOTE >= 0
|
| 403 |
+
? { kind: 'tree', x: TREE_X, index: TREE_NOTE, variant: 0,
|
| 404 |
+
seen: false, letterSeen: false, letterTaken: false, filed: null }
|
| 405 |
+
: null;
|
| 406 |
+
/** The middle of the nail on the tree, as a world point. Off the clip's pixels. */
|
| 407 |
+
const treePoint = () => {
|
| 408 |
+
const r = treeClip?.nailAt?.[0];
|
| 409 |
+
if (!TREE || !r) return null;
|
| 410 |
+
return [TREE.x - treeClip.pivotX + r[0] + r[2] / 2,
|
| 411 |
+
GROUND_Y - treeClip.pivotY + r[1] + r[3] / 2];
|
| 412 |
+
};
|
| 413 |
+
// PALE IN THE DARK. A house letter has a lit doorway beside it to draw the
|
| 414 |
+
// eye; a note on a lone tree has nothing, and would be walked past. So the
|
| 415 |
+
// scrap shows as paper catching what light there is β enough to say
|
| 416 |
+
// "something is pinned here", never enough to read. Above the no-mud floor.
|
| 417 |
+
const NOTE_IN_THE_DARK = '#9a9484'; // lum 148
|
| 418 |
+
const treeNoteDark = TREE && clips.letter
|
| 419 |
+
? (await tintFrames(clips.letter, NOTE_IN_THE_DARK, 'dark'))[TREE_NOTE] ?? null : null;
|
| 420 |
+
|
| 421 |
// --- the twelve --------------------------------------------------------
|
| 422 |
// Twelve shapes you can swing at, against the background watchers you cannot.
|
| 423 |
// Two populations doing different jobs: the watchers make it feel like a
|
|
|
|
| 912 |
seed: SEED,
|
| 913 |
houses: HOUSES, houseTint: PALETTE.WATCHERS,
|
| 914 |
houseClip, palaceClip, houseArt, doorPoint, keyPoint, letterPoint,
|
| 915 |
+
tree: TREE, treeClip, treeIconClip: clips.origin_tree ?? null, treeNoteDark, treePoint,
|
| 916 |
village, gateX: GATE_X, gateStopX: GATE_STOP_X,
|
| 917 |
villageGap: VILLAGE_GAP, bowlX0: BOWL.X0,
|
| 918 |
flame: { clip: clips.fire, player: createPlayer(clips.fire), frame: 0 },
|
|
|
|
| 945 |
h.letterSeen = false; h.letterTaken = false;
|
| 946 |
h.open = false; h.settled = false; h.keyTaken = false;
|
| 947 |
}
|
| 948 |
+
if (TREE) { TREE.seen = false; TREE.letterSeen = false; TREE.letterTaken = false; }
|
| 949 |
prevPickup = false;
|
| 950 |
prince.attacking = false;
|
| 951 |
prince.attackTime = 0;
|
|
|
|
| 1166 |
// β filing correctly from the far side of the village put a body on its
|
| 1167 |
// knees for ever. The flag covers the whole journey; it is cleared when
|
| 1168 |
// the door opens, and the cull takes over again.
|
| 1169 |
+
//
|
| 1170 |
+
// AND ONE WOKEN BY A WRONG PIN, for the same reason: it is on its way to
|
| 1171 |
+
// him, and a body skipped here would sit in the street half risen.
|
| 1172 |
+
if (!d.homing && !d.woken && Math.abs(d.x - prince.x) > W * 1.4) continue;
|
| 1173 |
// On screen together? The camera follows the prince, so this is the same
|
| 1174 |
// question as "can it see him".
|
| 1175 |
const sx = d.x - camera.x;
|
|
|
|
| 1222 |
// They are BODY sounds and never tool sounds, and audio.js says at
|
| 1223 |
// length why: a hammer-on-anvil clang would send every player to the
|
| 1224 |
// swinging shape, and the forge's fragment is deliberately on a hauler.
|
| 1225 |
+
//
|
| 1226 |
+
// AND ONE COMING FOR HIM IS HEARD COMING. Woken by a wrong pin it rushes
|
| 1227 |
+
// from wherever it fell, so its breathing plays off screen too: faint far
|
| 1228 |
+
// off, louder as it closes, on the side it is on. The walk home's drag
|
| 1229 |
+
// does the same, off the same numbers.
|
| 1230 |
+
if (d.frame !== wasFrame && d.clip?.soundFrames?.includes(d.frame) &&
|
| 1231 |
+
(ctx.inFrame || (d.woken && d.state === RUSH))) {
|
| 1232 |
+
const off = d.x - prince.x;
|
| 1233 |
+
const near = Math.max(0, 1 - Math.max(0, Math.abs(off) - W / 2) / Math.max(1, AUDIO.HOME_FOOT_REACH));
|
| 1234 |
+
audio?.drainedVoice?.(d.clip.name,
|
| 1235 |
+
ctx.inFrame ? 1 : AUDIO.HOME_FOOT_FAR + (1 - AUDIO.HOME_FOOT_FAR) * near,
|
| 1236 |
+
Math.max(-0.85, Math.min(0.85, off / 1400)));
|
| 1237 |
+
}
|
| 1238 |
// THE INSTANT IT NOTICES HIM. `revealed` latches and never unlatches, so
|
| 1239 |
// this fires once per shape per run β about twelve times, and every one
|
| 1240 |
// of them is a fight he chose by deciding to see.
|
|
|
|
| 1348 |
h.letterSeen = true;
|
| 1349 |
}
|
| 1350 |
}
|
| 1351 |
+
// THE NOTE ON THE TREE. No door to light first: the paper is all there is.
|
| 1352 |
+
if (TREE && !TREE.letterTaken) {
|
| 1353 |
+
const p = treePoint();
|
| 1354 |
+
if (p && torch.covers(p[0], p[1])) { TREE.seen = true; TREE.letterSeen = true; }
|
| 1355 |
+
}
|
| 1356 |
|
| 1357 |
// TAKING IT DOES NOT NEED THE TORCH STILL UP. Seeing latches; taking is a
|
| 1358 |
// separate act, exactly as it is for a page on the ground, which he can
|
|
|
|
| 1367 |
const p = letterPoint(h);
|
| 1368 |
if (p && Math.abs(prince.x - p[0]) <= TUNING.DOOR_REACH) { litLetter = h; break; }
|
| 1369 |
}
|
| 1370 |
+
// And the tree's, on the same terms. From here the band and E do not know
|
| 1371 |
+
// the difference: it reads as a letter and files as one, with its own page.
|
| 1372 |
+
if (!litLetter && TREE && !TREE.letterTaken && TREE.letterSeen) {
|
| 1373 |
+
const p = treePoint();
|
| 1374 |
+
if (p && Math.abs(prince.x - p[0]) <= TUNING.DOOR_REACH) litLetter = TREE;
|
| 1375 |
+
}
|
| 1376 |
|
| 1377 |
// AND THE KEY, WHICH NEEDS THE DOOR OPEN AND NOT THE TORCH UP.
|
| 1378 |
//
|
|
|
|
| 1584 |
flash.ms = 0;
|
| 1585 |
flash.thundered = false;
|
| 1586 |
}
|
| 1587 |
+
} else {
|
| 1588 |
+
// AND IF IT WAS NOT, THEY OBJECT. Played through, the level was
|
| 1589 |
+
// finished without reading a word: pin, see who walks, try the next
|
| 1590 |
+
// door. The walk home confirmed every guess instantly and for free,
|
| 1591 |
+
// which made it an answer key. Now a stranger's house wakes the
|
| 1592 |
+
// owner, and it comes for him. Guessing is still allowed; it costs.
|
| 1593 |
+
//
|
| 1594 |
+
// The page stays on the wrong wall. Putting it down does not take it
|
| 1595 |
+
// back β he does that, with E, as always.
|
| 1596 |
+
const owner = drained.find((d) => d.kept === holding && d.keptTaken);
|
| 1597 |
+
if (owner && owner.wake()) {
|
| 1598 |
+
// HEARD AT FULL STRENGTH, WHEREVER IT IS. On the side it is on, but
|
| 1599 |
+
// never quieter for being far: a faint gasp from across the village
|
| 1600 |
+
// and a rush thirty seconds later are two things nobody connects.
|
| 1601 |
+
// The breathing as it comes can fall away with distance. The
|
| 1602 |
+
// moment it wakes cannot.
|
| 1603 |
+
audio?.drainedWake?.(1, Math.max(-0.85, Math.min(0.85, (owner.x - prince.x) / 1400)));
|
| 1604 |
+
}
|
| 1605 |
}
|
| 1606 |
}
|
| 1607 |
}
|
src/tuning.js
CHANGED
|
@@ -533,6 +533,15 @@ export const TUNING = {
|
|
| 533 |
// they land every 0.9 of a screen, which is the same crowding the village
|
| 534 |
// was failing on, in the place that is supposed to be the quiet part.
|
| 535 |
ROAD_ENCOUNTERS: 3,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
|
| 537 |
// THE VILLAGE. The road ends and the bowl begins. This replaces the ending
|
| 538 |
// that used to be here β thirty-four watchers stepping onto the path until he
|
|
|
|
| 533 |
// they land every 0.9 of a screen, which is the same crowding the village
|
| 534 |
// was failing on, in the place that is supposed to be the quiet part.
|
| 535 |
ROAD_ENCOUNTERS: 3,
|
| 536 |
+
// THE TREE WITH THE NOTE ON IT, as a share of the road. Two-thirds: past
|
| 537 |
+
// the first drained, so "they" has somebody to mean, and before the first
|
| 538 |
+
// house, so it is read before the first fight.
|
| 539 |
+
TREE_AT: 0.66,
|
| 540 |
+
// AND NOBODY STANDS NEAR IT. Placed first, the way the doors are, and every
|
| 541 |
+
// road drained kept this far off. Not a guess: he reads it from about 300px
|
| 542 |
+
// away, and the torch lights a figure up to 339 ahead of him and 214 behind β
|
| 543 |
+
// so lighting the note can never wake anybody, from either side.
|
| 544 |
+
TREE_CLEAR: 700,
|
| 545 |
|
| 546 |
// THE VILLAGE. The road ends and the bowl begins. This replaces the ending
|
| 547 |
// that used to be here β thirty-four watchers stepping onto the path until he
|