system HF Staff commited on
Commit
9986f05
Β·
verified Β·
1 Parent(s): 776f3e2

268252f: Wrong doors wake the owner, and a note on the tree says the rule

Browse files
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": 6,
1074
  "fps": 1,
1075
  "variants": true,
1076
  "native": [
@@ -1082,7 +1137,7 @@
1082
  48
1083
  ],
1084
  "silhouette": "auto",
1085
- "_frames": "Five village papers and one palace one. The sixth is the order, and it is cleaner and squarer than the others because the palace does not write on scraps."
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, v = AUDIO.DRAINED_VOICE) => {
 
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: (v = AUDIO.WAKE) => {
518
  if (!enabled) return;
519
  const c = ensure();
520
  if (!c || c.state !== 'running') return;
521
- playSlice('wake', v);
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 DOES NOTHING. The page is on the wall, right or
1587
- // wrong, and the body stays exactly where it fell.
 
 
 
1588
  atDoor(1, 0);
1589
  tap({ sheaf: true });
1590
- drive(3);
 
 
 
 
1591
  const wrong = {
1592
  onTheWall: s.houses[1].filed === 0,
1593
- bodyStaysDown: owner.state === 'DOWNED',
 
 
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 - owner.spawnX);
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
- seconds: +(ticks / 60).toFixed(1),
 
 
 
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.bodyStaysDown && wrong.nothingOpened &&
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
- theWrongFrameDoesNothing: wrong,
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 FEEDBACK, EVER. Nothing in the state says right or wrong, and an
2047
- // empty plot is not flagged. Four filed and one empty is the finding,
2048
- // and it has to look exactly like four filed and one not done yet.
 
 
 
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
- // TEN, EXACTLY. Nine village strings and his orders. An exact equality
2460
- // rather than a floor, so adding a string and not looking at it fails
2461
- // here rather than on somebody's phone.
2462
- pass: rows.length === 10 && worst <= BAND.MAX_LINES && highest >= BAND.TOP_FLOOR,
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
- if (!ctx.inFrame) break;
 
 
 
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 six letters come from a house in this
928
- // village and show it. The sixth came from the palace, which is not a
929
- // building here and has no entry in `houses` -- so no house means the
930
- // palace, and that fallback IS the rule rather than a guard around one.
931
- const hc = house ? s.houseClip : s.palaceClip;
 
 
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
- if (house.filed == null) {
 
 
 
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
- if (!d.homing && Math.abs(d.x - prince.x) > W * 1.4) continue;
 
 
 
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
- if (ctx.inFrame && d.frame !== wasFrame &&
1189
- d.clip?.soundFrames?.includes(d.frame)) audio?.drainedVoice?.(d.clip.name);
 
 
 
 
 
 
 
 
 
 
 
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