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

77dd24d: A source check that checks, and a dev server that answers

Browse files

Published from cloudronin/astara@77dd24d4b0f0c24be9c7a520ae4810a52ee8def9

Files changed (10) hide show
  1. README.md +9 -6
  2. assets/origin_tree/lit_00.svg +0 -6
  3. manifest.json +44 -31
  4. src/assets.js +5 -4
  5. src/checks.js +702 -234
  6. src/input.js +4 -3
  7. src/main.js +1 -1
  8. src/render.js +71 -104
  9. src/scene_level.js +204 -156
  10. src/tuning.js +11 -2
README.md CHANGED
@@ -33,9 +33,12 @@ people outside knew about whoever lived there. Knock down a carrier and you get
33
 
34
  Neither one names anybody. The gap between them is the whole game.
35
 
36
- **Take the letter off the nail and what is left is the patch of cleaner wall it
37
- kept the weather off for sixty years.** That is where a fragment goes. Stand at
38
- the door, scroll to one, press take, and it is pinned there.
 
 
 
39
 
40
  ## And then something happens
41
 
@@ -43,9 +46,9 @@ the door, scroll to one, press take, and it is pinned there.
43
  up, walks home, opens their own door and stands in it.** They do not speak and
44
  they do not move again. It is the only act in the level that is not taking.
45
 
46
- Put the wrong one there and nothing happens at all. The page sits on the wall
47
- and the body stays where it fell. You can take it back out and try somewhere
48
- else β€” nobody stops you, and nothing tells you which it was.
49
 
50
  **Inside every house, hanging by the door, there is a key to the gate.** You can
51
  see it from the first time you light that door and you cannot reach it, because
 
33
 
34
  Neither one names anybody. The gap between them is the whole game.
35
 
36
+ **The letter stays on its nail. Under it is a second nail and a lighter patch of
37
+ wall, where their own paper used to hang.** That is where a fragment goes. Stand
38
+ at the door with one in your hand, press take, and it is pinned there.
39
+
40
+ The door is where you judge: the question nailed in front of you, the answer in
41
+ your hand. Hold `Q` to read your page, let go to read the door again.
42
 
43
  ## And then something happens
44
 
 
46
  up, walks home, opens their own door and stands in it.** They do not speak and
47
  they do not move again. It is the only act in the level that is not taking.
48
 
49
+ Put the wrong one there and they get up anyway β€” and come for you. The page
50
+ stays on the wrong wall until you take it back down and try somewhere else.
51
+ Guessing is allowed. It costs.
52
 
53
  **Inside every house, hanging by the door, there is a key to the gate.** You can
54
  see it from the first time you light that door and you cannot reach it, because
assets/origin_tree/lit_00.svg DELETED
manifest.json CHANGED
@@ -967,39 +967,72 @@
967
  480,
968
  760
969
  ],
970
- "nailAt": [
971
  [
972
- 288,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
973
  704,
974
  56,
975
  76
976
  ],
977
  [
978
- 288,
979
  704,
980
  56,
981
  76
982
  ],
983
  [
984
- 288,
985
  704,
986
  56,
987
  76
988
  ],
989
  [
990
- 288,
991
  704,
992
  56,
993
  76
994
  ],
995
  [
996
- 288,
997
  704,
998
  56,
999
  76
1000
  ]
1001
  ],
1002
- "_nailAt_note": "ONE SPOT BESIDE EVERY DOOR, in the clip's own pixels, and it does three jobs in turn: the house's letter hangs there; take it and what is left is the lighter patch of wall the paper kept the weather off for sixty years; pin a fragment and it sits in that patch. WHAT IT REPLACES: a letter rect and, under it, a bright square frame β€” which read as a noticeboard, and no village in any century had one bolted to every house. THE PATCH IS EARNED: a house you have not read has a letter and no slot, so taking the letter is the thing that shows you where a fragment goes. That is the affordance the frame was trying to be. AND IT IS LOW, NOT HIGH ON THE DOOR: with the torch in her standing drawing's hand it hangs at hip height, the top of the light fell from 310 to 506, and everything above the door is dark. This is the rectangle the frame took four tries to find. GENERATED by gen_placeholders.js from the same geometry it draws the doorway from, never hand-written.",
 
1003
  "keyAt": [
1004
  [
1005
  380,
@@ -1032,7 +1065,7 @@
1032
  62
1033
  ]
1034
  ],
1035
- "_keyAt_note": "WHERE THE KEY HANGS, in the clip's own pixels, one per house. Just inside the left jamb and low β€” inside, because a key to your own front door hangs inside your own front door, and low for the same reason the frame and the letter are low: her standing drawing holds the torch at hip height, so the top of the light fell from 310 to 506 and everything above the door is dark. GENERATED, not written down: gen_placeholders.js publishes these out of the same arch geometry it draws the doorway from, so moving a door moves the key with it. letterAt was hand-authored and drifted from the art twice.",
1036
  "_doorAt_note": "Where the torch has to fall for the door to count as looked at, in the clip's own pixels. Same point in all five, because the door is at the middle of every facade. Move the door in the art and the reveal follows it.",
1037
  "bleedEdges": [
1038
  "bottom"
@@ -1073,7 +1106,7 @@
1073
  210,
1074
  1180
1075
  ],
1076
- "nailAt": [
1077
  [
1078
  185,
1079
  984,
@@ -1081,27 +1114,7 @@
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": {
@@ -1137,7 +1150,7 @@
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.",
 
967
  480,
968
  760
969
  ],
970
+ "letterAt": [
971
  [
972
+ 282,
973
+ 612,
974
+ 56,
975
+ 76
976
+ ],
977
+ [
978
+ 272,
979
+ 612,
980
+ 56,
981
+ 76
982
+ ],
983
+ [
984
+ 282,
985
+ 612,
986
+ 56,
987
+ 76
988
+ ],
989
+ [
990
+ 272,
991
+ 612,
992
+ 56,
993
+ 76
994
+ ],
995
+ [
996
+ 272,
997
+ 612,
998
+ 56,
999
+ 76
1000
+ ]
1001
+ ],
1002
+ "slotAt": [
1003
+ [
1004
+ 282,
1005
  704,
1006
  56,
1007
  76
1008
  ],
1009
  [
1010
+ 272,
1011
  704,
1012
  56,
1013
  76
1014
  ],
1015
  [
1016
+ 282,
1017
  704,
1018
  56,
1019
  76
1020
  ],
1021
  [
1022
+ 272,
1023
  704,
1024
  56,
1025
  76
1026
  ],
1027
  [
1028
+ 272,
1029
  704,
1030
  56,
1031
  76
1032
  ]
1033
  ],
1034
+ "_letterAt_note": "THE LETTER, in the clip's own pixels, one per house. It never comes down: letters stay where they were left, and fragments come off bodies. So the door is where you judge β€” the question nailed in front of you, the fragment in your hand. STACKED OVER THE SLOT, the question above the answer: the only lit, empty wall is the ~118px column between where he stands and the arch's left jamb, and two pages side by side put one behind his shoulder. Its middle is 250px off the ground, nearer the flame. GENERATED by gen_placeholders.js from the same arch geometry it draws the doorway from, never hand-written.",
1035
+ "_slotAt_note": "THE SLOT, in the clip's own pixels: a second nail under the letter and the lighter patch where their own paper used to hang. Pinning a fragment is putting that paper back. THERE FROM THE FIRST LIGHT, because it is the affordance β€” a bare nail beside a letter says something goes here, a lighter patch says something was here. A VALUE, NOT AN OUTLINE: no border and no glow. At the height the single nail used to hang, 22px off the jamb so the forge's anvil horn clears it by 6. GENERATED, with letterAt.",
1036
  "keyAt": [
1037
  [
1038
  380,
 
1065
  62
1066
  ]
1067
  ],
1068
+ "_keyAt_note": "WHERE THE KEY HANGS, in the clip's own pixels, one per house. Just inside the left jamb and low β€” inside, because a key to your own front door hangs inside your own front door, and low because it was hung when the torch was held at hip height, and the light still finds it there from where he stands to read the letter. GENERATED, not written down: gen_placeholders.js publishes these out of the same arch geometry it draws the doorway from, so moving a door moves the key with it. The first hand-authored letter rect drifted from the art twice.",
1069
  "_doorAt_note": "Where the torch has to fall for the door to count as looked at, in the clip's own pixels. Same point in all five, because the door is at the middle of every facade. Move the door in the art and the reveal follows it.",
1070
  "bleedEdges": [
1071
  "bottom"
 
1106
  210,
1107
  1180
1108
  ],
1109
+ "letterAt": [
1110
  [
1111
  185,
1112
  984,
 
1114
  76
1115
  ]
1116
  ],
1117
+ "_letterAt": "The note, on the trunk wherever the trunk went, at a house slot's height above the ground, so the torch finds it from where a player already stands at a door. It never comes down and there is no slot: nothing goes back to a tree. The generator prints it.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1118
  "silhouette": "auto"
1119
  },
1120
  "kept": {
 
1150
  48
1151
  ],
1152
  "silhouette": "auto",
1153
+ "_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 on 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."
1154
  },
1155
  "key": {
1156
  "_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/assets.js CHANGED
@@ -195,10 +195,11 @@ export async function loadClips(manifest) {
195
  variantScale: def.variantScale ?? null,
196
  keptDrop: def.keptDrop ?? null,
197
  doorAt: def.doorAt ?? null,
198
- // One spot beside each door: the letter, then the patch it leaves, then
199
- // the page filed into that patch. Generated, never hand-written β€” the
200
- // two rects this replaces included the one that drifted.
201
- nailAt: def.nailAt ?? null,
 
202
  keyAt: def.keyAt ?? null,
203
  mapPivot: def.mapPivot ?? null,
204
  plots: def.plots ?? null,
 
195
  variantScale: def.variantScale ?? null,
196
  keptDrop: def.keptDrop ?? null,
197
  doorAt: def.doorAt ?? null,
198
+ // Two spots beside each door, stacked: the letter, which never comes
199
+ // down, and under it the slot a fragment is pinned into. Generated, never
200
+ // hand-written β€” the rects these replace included one that drifted.
201
+ letterAt: def.letterAt ?? null,
202
+ slotAt: def.slotAt ?? null,
203
  keyAt: def.keyAt ?? null,
204
  mapPivot: def.mapPivot ?? null,
205
  plots: def.plots ?? null,
src/checks.js CHANGED
@@ -14,7 +14,7 @@
14
  // continuous tone allowed in the background the proxy stops working, so the two
15
  // tests below ask the real question directly, in the two places it matters.
16
 
17
- import { TUNING, BOWL, TORCH } from './tuning.js';
18
  import { HOUSE_OF, laneLook, figureMidY } from './drained.js';
19
  import { BAND, linesNeeded, bandBox } from './band.js';
20
  import { GROUND_Y, W, H } from './scene.js';
@@ -715,14 +715,21 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
715
  /**
716
  * THE SHEAF. A pile of loose paper, and every claim here is about paper.
717
  *
718
- * The map is page one. Everything after it is in the order he picked it up β€”
719
- * no grouping, no sorting, no index and no tabs, because all of those are the
720
- * game telling him what he has, and what he has is the thing he is meant to
721
- * work out.
 
 
 
 
 
 
722
  */
723
  function sheafPages() {
724
  scene.reset();
725
  const sheaf = s.sheaf;
 
726
 
727
  // 1. IT STARTS WITH HIS ORDERS AND NOTHING ELSE.
728
  //
@@ -736,83 +743,63 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
736
  order.kind === 'letter' && !s.houses[order.house] &&
737
  s.textFor(order) === s.letters.byHouse[order.house].letter;
738
 
739
- // 2. THE PLAIN TORCH READS IT WHERE IT LIES, and taking it is a separate
740
- // act. It used to fall into the sheaf the instant the door lit, which
741
- // made raising the sheaf the only way to see a word of the writing.
742
- // Now it is an object in the room: look at it, then take it.
743
- s.prince.x = s.prince.prevX = s.houses[1].x - 300;
744
- s.prince.facing = 1;
745
- drive(2, { torch: true });
746
- const readsIt = !!s.band.text && s.band.text === s.letters.byHouse[1].letter;
747
- const notYetFiled = sheaf.pages.length === 1; // the order, and nothing taken yet
748
-
749
- drive(0.4, { torch: true, pickup: true });
750
- const letterFiled = sheaf.pages.length === 2 &&
751
- sheaf.pages[1].kind === 'letter' && sheaf.pages[1].house === 1 &&
752
- sheaf.index === 1 && s.houses[1].letterTaken;
753
-
754
- // And it is gone from the wall: taking it twice cannot happen.
755
- drive(0.4, { torch: true, pickup: true });
756
- const takenOnce = sheaf.pages.length === 2;
757
-
758
- // 2b. AND THE TORCH DOES NOT HAVE TO STILL BE UP. Seeing latches; taking is
759
- // a separate act, the same as it is for a page on the ground. It used
760
- // to need the light held, which on a phone made the letter impossible:
761
- // TORCH and TAKE are both on the left wheel and one thumb cannot hold
762
- // one while pressing the other.
763
- const h2 = s.houses[2];
764
- const [d2] = s.doorPoint(h2);
765
- s.prince.x = s.prince.prevX = d2 - 300;
766
- drive(1.4, { torch: true });
767
- drive(0.5); // torch down, still stood there
768
- const offeredInTheDark = s.canTakeLetter && s.torch.raise < 0.01;
769
- drive(0.4, { pickup: true });
770
- const takenInTheDark = h2.letterTaken;
771
-
772
- // But not from anywhere. Across the village it is not his to take.
773
- const h3 = s.houses[3];
774
- const [d3] = s.doorPoint(h3);
775
- s.prince.x = s.prince.prevX = d3 - 300;
776
- drive(1.4, { torch: true });
777
- s.prince.x = s.prince.prevX = d3 - 1500;
778
- drive(0.4);
779
- const notFromAcrossTheVillage = !s.canTakeLetter && s.houses[3].letterSeen;
780
 
781
  // 3. And running off the front of a one-page pile stays on that page.
782
  drive(0.5, { sheaf: true, left: true });
783
  const holdsAtTheFront = sheaf.index === 0 && !!s.band.text;
784
 
785
- // 4. A FLIP WITH A BEAT. Holding an arrow walks the pile at reading speed,
786
- // not at frame rate. Three more pages, then hold right for a second and
787
- // count the turns: at 60fps a free browse would be sixty.
788
- for (let k = 2; k < 5; k++) {
789
- const h = s.houses[k];
790
- s.prince.x = s.prince.prevX = h.x - 300;
791
- drive(1.2, { torch: true });
792
- drive(0.4, { torch: true, pickup: true });
793
- }
794
- // The order he FOUND them in, which is the order of everything after his
795
- // orders β€” page zero was in his hand before he left the palace.
796
- const foundOrder = sheaf.pages.slice(1).map((p) => p.house).join(',');
 
 
 
 
 
797
  sheaf.index = 0;
798
  const before = sheaf.turns;
799
  drive(1, { sheaf: true, right: true });
800
  const turnsPerSecond = sheaf.turns - before;
801
 
802
- // 5. IT DOES NOT WRAP. Running off the end of a pile of paper does nothing.
803
  drive(4, { sheaf: true, right: true });
804
  const stopsAtTheEnd = sheaf.index === sheaf.pages.length - 1;
805
  drive(6, { sheaf: true, left: true });
806
  const stopsAtTheStart = sheaf.index === 0;
807
 
808
- // 6. THE INDEX SURVIVES RELEASE. He reopens where he left off.
809
  drive(1.2, { sheaf: true, right: true });
810
  const wasOn = sheaf.index;
811
  drive(1); // sheaf down, walking about
812
  drive(0.6, { sheaf: true });
813
  const reopensWhereHeLeftIt = sheaf.index === wasOn && wasOn > 0;
814
 
815
- // 7. A `?` RESOLVES THE MOMENT HE PAGES TO IT, and only the one he turned
816
  // to. Two blind takes, open the second: it fills, the first stays blank.
817
  // The pages BEHIND the one in his hand are what the unknown state is for.
818
  scene.reset();
@@ -826,7 +813,7 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
826
  s.band.text === s.letters.byHouse[2].fragment;
827
  const resolvedTheOneHeIsOn = !s.prince.inventory[0].seen;
828
 
829
- // 8. And the one still behind him reads as blank paper β€” no words in the
830
  // band for a page he has not turned to.
831
  sheaf.index = 1; // the FIRST fragment, past the order
832
  s.band.text = null;
@@ -837,21 +824,20 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
837
 
838
  return {
839
  name: 'the sheaf pages',
840
- pass: startsWithTheOrder && letterFiled && readsIt && notYetFiled && takenOnce &&
841
- offeredInTheDark && takenInTheDark && notFromAcrossTheVillage &&
842
- holdsAtTheFront &&
843
- foundOrder === '1,2,3,4' && turnsPerSecond >= 3 && turnsPerSecond <= 5 &&
844
  stopsAtTheEnd && stopsAtTheStart && reopensWhereHeLeftIt &&
845
  blankWhileUnknown && resolvedOnPage && resolvedTheOneHeIsOn &&
846
  opensWhenHeTurnsToIt,
847
- startsWithTheOrder, letterFiled,
848
- theTorchAloneReadsIt: readsIt,
849
- andLookingDoesNotTakeIt: notYetFiled,
850
- takenOnceOnly: takenOnce,
851
- takeableWithTheTorchDown: offeredInTheDark && takenInTheDark,
852
- butNotFromAcrossTheVillage: notFromAcrossTheVillage,
853
  holdsAtTheFrontOfThePile: holdsAtTheFront,
854
- foundOrder, turnsPerSecondHeld: turnsPerSecond,
 
 
855
  stopsAtTheEnd, stopsAtTheStart, reopensWhereHeLeftIt,
856
  blankUntilHeTurnsToIt: blankWhileUnknown, resolvesOnPage: resolvedOnPage,
857
  leavesThePagesBehindHimBlank: resolvedTheOneHeIsOn,
@@ -1031,7 +1017,11 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1031
  ticks++;
1032
  }
1033
  drive(1.4, { torch: true });
1034
- tap(); tap(); // the letter first, then the key
 
 
 
 
1035
  keyRows.push({ house: s.houses[k].kind, at: Math.round(kx),
1036
  open: s.houses[k].open, taken: s.prince.keys > before });
1037
  }
@@ -1046,6 +1036,7 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1046
  s.camera.reset(s.prince.x);
1047
  drive(1.6, { torch: true });
1048
  const heldBefore = s.prince.keys;
 
1049
  tap(); tap(); tap();
1050
  const residue = { lit: loom.seen, open: loom.open, taken: loom.keyTaken,
1051
  gotNothing: s.prince.keys === heldBefore };
@@ -1206,19 +1197,14 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1206
  /**
1207
  * Stand at house i with the sheaf up, on the page for fragment k.
1208
  *
1209
- * THE LETTER COMES OFF FIRST, because until it does there is no slot: the
1210
- * nail beside the door holds the letter, and the patch it leaves is where a
1211
- * fragment goes. That is the order a player does it in too.
1212
  */
1213
  function atDoor(i, k = null) {
1214
  s.prince.x = s.prince.prevX = s.houses[i].x - 300;
1215
  s.prince.facing = 1;
1216
  s.camera.reset(s.prince.x);
1217
- if (!s.houses[i].letterTaken) {
1218
- drive(1.4, { torch: true });
1219
- drive(0.2, { pickup: true });
1220
- drive(0.15);
1221
- }
1222
  if (k !== null) {
1223
  s.sheaf.index = s.sheaf.pages.findIndex((p) => p.kind === 'fragment' && p.kept === k);
1224
  }
@@ -1249,14 +1235,8 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1249
  drive(0.6); // Q released
1250
  const stillFragment = held()?.kind === 'fragment' && !s.sheaf.up;
1251
 
1252
- // A letter is a different shape in the hand, and it can be on top too β€”
1253
- // but one has to be in the pile first, and armed() only takes fragments.
1254
- s.prince.x = s.prince.prevX = s.houses[2].x - 300;
1255
- s.prince.facing = 1;
1256
- s.camera.reset(s.prince.x);
1257
- drive(1.4, { torch: true });
1258
- drive(0.2, { pickup: true });
1259
- drive(0.15);
1260
  const li = s.sheaf.pages.findIndex((p) => p.kind === 'letter');
1261
  s.sheaf.index = li;
1262
  drive(0.6);
@@ -1329,16 +1309,13 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1329
  const A = 0, B = 2;
1330
  const h = s.houses[H];
1331
 
1332
- // 1. A letter on the nail, anything on top -> takes the letter.
1333
- put(0); stand(H, withQ);
1334
- // the door has to have been looked at at least once for anything to be
1335
- // offered, which is the latch, not a mode
1336
- drive(1.4, { torch: true });
1337
  stand(H, withQ);
1338
- press();
1339
- const tookLetter = h.letterTaken && h.filed == null;
1340
 
1341
- // 2. A bare patch, a fragment on top -> pins it.
1342
  put(A); stand(H, withQ); press();
1343
  const pinned = h.filed === A;
1344
 
@@ -1366,13 +1343,12 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1366
  const forge = s.houses[F];
1367
  put(0); stand(F, withQ);
1368
  drive(1.4, { torch: true });
1369
- stand(F, withQ); press(); // letter off
1370
  put(0); stand(F, withQ); press(); // the forge's own, so it settles
1371
  const settled = forge.settled && forge.filed === 0;
1372
  put(B); stand(F, withQ); press(); // try to swap it
1373
  const lockHeld = forge.filed === 0 && settled;
1374
 
1375
- rows.push({ withQ, tookLetter, pinned, swapped, tookBackSame,
1376
  tookBackWithLetter, bareAndLetterDoesNothing, lockHeld });
1377
  }
1378
  scene.reset();
@@ -1420,7 +1396,7 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1420
  return withOverlayOff(() => {
1421
  const clip = s.houseClip;
1422
  const patch = (h) => {
1423
- const [nx, ny, nw, nh] = clip.nailAt[h.variant];
1424
  return [Math.round(h.x - s.camera.x - clip.pivotX + nx),
1425
  Math.round(GROUND_Y - clip.pivotY + ny), nw, nh];
1426
  };
@@ -1470,10 +1446,6 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1470
  s.prince.facing = 1;
1471
  s.camera.reset(s.prince.x);
1472
  drive(1.4, { torch: true });
1473
- if (!h.letterTaken) {
1474
- drive(0.2, { torch: true, pickup: true });
1475
- drive(0.15, { torch: true });
1476
- }
1477
  drive(0.5, { torch: true }); // settled, and staying up
1478
  };
1479
  const put = (k) => {
@@ -1486,25 +1458,25 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1486
  const forge = s.houses[0];
1487
  stand(forge);
1488
 
1489
- // 1. A LETTER ON TOP IS A BARE PATCH, and two renders of it back to back
1490
  // are the grain floor everything else is measured against.
1491
  put(null);
1492
  const bare = brightness(shot(forge));
1493
  const noise = Math.abs(brightness(shot(forge)) - bare);
1494
 
1495
- // 1b. AND AN EMPTY PATCH BREATHES.
1496
  //
1497
- // The ghost below only helps somebody already standing here with the
1498
- // right thing in hand. Nothing walked them over: they lit a door, saw
1499
- // a slightly cleaner rectangle, and had no reason to think it was
1500
- // anything β€” which is what actually happened to the first person who
1501
- // played it. The breath is the same one a dropped scrap has and a key
1502
- // has, at the same numbers, and it means what it has always meant.
1503
  //
1504
  // Measured over one full cycle of KEPT_PULSE_HZ, on a box wider than
1505
- // the patch so the halo around it is counted too.
1506
  const wide = (h) => {
1507
- const [nx, ny, nw, nh] = clip.nailAt[h.variant];
1508
  return [Math.round(h.x - s.camera.x - clip.pivotX + nx) - 26,
1509
  Math.round(GROUND_Y - clip.pivotY + ny) - 26, nw + 52, nh + 52];
1510
  };
@@ -1518,7 +1490,7 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1518
  s.clock = 3.0;
1519
  return Math.round(Math.max(...seen) - Math.min(...seen));
1520
  };
1521
- const emptyBreathes = breath(forge);
1522
 
1523
  // 2. A SCRAP ON TOP AND THE WALL SHOWS IT.
1524
  put(0);
@@ -1536,10 +1508,7 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1536
  const pinned = brightness(shot(forge));
1537
  const ghostIsNotThePin = ghosted - pinned; // solid, so darker again
1538
 
1539
- // AND IT STOPS. An empty place asks its own question; a full one has
1540
- // nothing left to ask. Same reason the gate's sockets never needed
1541
- // explaining β€” and why the loom house goes on asking all level, with
1542
- // nobody left to answer it.
1543
  const filledIsStill = breath(forge);
1544
 
1545
  // 4. AND IT IS OVER. Lightning, not a filter.
@@ -1564,11 +1533,11 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1564
  pass: ghostShows > Math.max(8, noise * 3) &&
1565
  ghostIsNotThePin > Math.max(8, noise * 3) &&
1566
  wrongGhost > Math.max(8, noise * 3) &&
1567
- emptyBreathes > 8 && filledIsStill <= 2 &&
1568
  struck && quiet && gone,
1569
  flickerMovesTheMeanBy: noise,
1570
- anEmptyPatchBreathesBy: emptyBreathes,
1571
- andAFilledOneIsStill: filledIsStill,
1572
  theWallPreviewsTheScrap: ghostShows,
1573
  andItPreviewsAWrongOneToo: wrongGhost,
1574
  theGhostIsNotThePin: ghostIsNotThePin,
@@ -1723,37 +1692,15 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1723
  const visible = rows.every((r) => r.ink > 120 && r.light > 200 && r.litByTheTorch);
1724
  const noneOfferedWhileShut = rows.every((r) => !r.offered);
1725
 
1726
- // 1b. AND THE DOOR CARRIES THREE THINGS NOW. The nail, the key and the
1727
- // room all live in the same arch, and all three have to be inside
1728
- // the light from where he stops β€” which is the constraint that took
1729
- // four tries to satisfy for two of them.
1730
- const three = s.houses.map((h) => {
1731
- const [nx, ny, nw, nh] = clip.nailAt[h.variant];
1732
- const [kx, ky, kw, kh] = clip.keyAt[h.variant];
1733
- s.prince.x = s.prince.prevX = h.x - 300;
1734
- s.prince.facing = 1;
1735
- s.camera.reset(s.prince.x);
1736
- drive(1.6, { torch: true });
1737
- const np = s.letterPoint(h);
1738
- const kp = s.keyPoint(h);
1739
- return {
1740
- house: h.kind,
1741
- // Rectangles, in the clip's own pixels. They may not touch.
1742
- clear: nx + nw <= kx || kx + kw <= nx || ny + nh <= ky || ky + kh <= ny,
1743
- nailLit: s.torch.covers(np[0], np[1]),
1744
- keyLit: s.torch.covers(kp[0], kp[1]),
1745
- // And from as far back as the letter can be taken from, which is
1746
- // wider than where he stands to read it.
1747
- nailInReach: Math.abs(s.prince.x - np[0]) <= TUNING.DOOR_REACH,
1748
- keyInReach: Math.abs(s.prince.x - kp[0]) <= TUNING.DOOR_REACH,
1749
- };
1750
- });
1751
- const doorCarriesThree = three.every((r) =>
1752
- r.clear && r.nailLit && r.keyLit && r.nailInReach && r.keyInReach);
1753
 
1754
  // 2. NOT HIS WHILE THE DOOR IS SHUT.
 
 
1755
  s.prince.x = s.prince.prevX = s.houses[0].x - 300;
1756
  drive(1.6, { torch: true });
 
1757
  tap(); tap();
1758
  const shut = { keys: s.prince.keys, taken: s.houses[0].keyTaken };
1759
 
@@ -1767,7 +1714,8 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1767
  s.prince.x = s.prince.prevX = s.houses[k].x - 300;
1768
  s.camera.reset(s.prince.x);
1769
  drive(1.6, { torch: true });
1770
- tap(); tap(); // the letter first, then the key
 
1771
  }
1772
  const open = { keys: s.prince.keys, taken: s.houses.map((h) => h.keyTaken) };
1773
 
@@ -1779,21 +1727,20 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1779
  s.camera.reset(s.prince.x);
1780
  drive(1.6, { torch: true });
1781
  const offeredAtTheLoom = s.canTakeKey;
 
1782
  tap(); tap(); tap();
1783
  const residue = { open: s.houses[3].open, taken: s.houses[3].keyTaken,
1784
  offered: offeredAtTheLoom, keys: s.prince.keys };
1785
 
1786
  return {
1787
  name: 'the key is behind the door',
1788
- pass: visible && noneOfferedWhileShut && doorCarriesThree &&
1789
  shut.keys === 0 && !shut.taken &&
1790
  open.keys === 4 && open.taken.filter(Boolean).length === 4 &&
1791
  !residue.open && !residue.taken && !residue.offered && residue.keys === 4 &&
1792
  carriers.length === 4,
1793
  visibleInEveryLitDoorway: visible,
1794
  andNotOfferedWhileShut: noneOfferedWhileShut,
1795
- theNailTheKeyAndTheRoomAllClearEachOther: doorCarriesThree,
1796
- three,
1797
  shut, open,
1798
  theLoomHouseNeverOpens: residue,
1799
  rows,
@@ -1962,9 +1909,7 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1962
  */
1963
  function markingIsAClaim() {
1964
  scene.reset();
1965
- // Stand at house i's door, letter off the nail, sheaf up. The letter has
1966
- // to come off first now: the nail holds it, and the patch it leaves is the
1967
- // slot. See claim 1b.
1968
  const at = (i) => atDoor(i);
1969
  const mark = () => drive(0.3, { sheaf: true, pickup: true });
1970
  const filed = () => s.houses.map((h) => h.filed);
@@ -1983,29 +1928,24 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
1983
  // nail is still bare.
1984
  const nothingAutoFiles = filed().every((f) => f == null);
1985
 
1986
- // 1b. AND THERE IS NOWHERE TO FILE UNTIL THE LETTER IS OFF THE NAIL.
 
1987
  //
1988
- // One spot beside each door, and the letter is on it. Take the letter
1989
- // and the patch of cleaner wall it kept the weather off for sixty
1990
- // years is the slot. A house he has not read has nowhere to put
1991
- // anything, so taking the letter is the thing that shows him where β€”
1992
- // an affordance earned rather than drawn, which is why the frame went.
1993
  const unread = s.houses[2];
 
1994
  s.prince.x = s.prince.prevX = unread.x - 300;
1995
  s.prince.facing = 1;
1996
  s.camera.reset(s.prince.x);
1997
  s.sheaf.index = fragA;
1998
  drive(1.2, { sheaf: true });
1999
- const offeredBeforeReading = s.canMark;
2000
- const litNotRead = unread.seen && !unread.letterTaken;
2001
  mark();
2002
- // AND THE PRESS IS NOT WASTED. MARK is not offered, so the same button
2003
- // does the thing that IS available at a door with a letter still on it:
2004
- // it takes the letter. Which is what makes the slot. One button, and it
2005
- // walks a player through the order on its own β€” letter first, claim after.
2006
- const tookTheLetterInstead = unread.letterTaken && unread.filed == null;
2007
- const needsTheLetterOff = !offeredBeforeReading && litNotRead &&
2008
- tookTheLetterInstead;
2009
 
2010
  // 2. He files one at a lit door.
2011
  s.sheaf.index = fragA; at(0); mark();
@@ -2083,14 +2023,13 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
2083
 
2084
  return {
2085
  name: 'marking is a claim',
2086
- pass: nothingAutoFiles && needsTheLetterOff && noSheafNeeded &&
2087
  afterFirst[0] === 2 && afterReplace[0] === 3 &&
2088
  afterMove[0] == null && afterMove[1] === 3 && onlyOnce &&
2089
  needsADoor && mapCannotBeFiled && noVerdict &&
2090
  frozenAtTheGate,
2091
  nothingFilesItself: nothingAutoFiles,
2092
- andThereIsNoSlotUntilTheLetterIsOff: needsTheLetterOff,
2093
- theSamePressTakesTheLetterInstead: tookTheLetterInstead,
2094
  afterFirst, afterReplace, afterMove,
2095
  oneFragmentOneHouse: onlyOnce,
2096
  needsALitDoor: needsADoor,
@@ -2307,12 +2246,13 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
2307
  * at a door, which is the thing the cue was meant to teach. So the question
2308
  * is asked in the world.
2309
  *
2310
- * IT WAS A FRAME AND IT IS NOW A NAIL. A bright square box bolted to every
2311
  * house read as a noticeboard, and no village in any century had one. A
2312
  * letter nailed up is how you reached somebody who was not home, anywhere,
2313
- * ever β€” and when the letter comes off, the lighter patch of wall it kept the
2314
- * weather off is the slot. The frame was drawn; the patch is EARNED, because
2315
- * a house he has not read has a letter and no slot at all.
 
2316
  *
2317
  * THE GATE HAS ALWAYS WORKED THIS WAY. Four empty sockets on the leaf, and
2318
  * nobody has ever needed telling what they are for.
@@ -2320,12 +2260,14 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
2320
  * Measured in pixels, because it is entirely a claim about what is on screen.
2321
  */
2322
  function theRoomAsks() {
2323
- const nailRect = (house) => {
2324
  const clip = s.houseClip;
2325
- const [fx, fy, fw, fh] = clip.nailAt[house.variant];
2326
  return [Math.round(house.x - s.camera.x - clip.pivotX + fx),
2327
  Math.round(GROUND_Y - clip.pivotY + fy), fw, fh];
2328
  };
 
 
2329
  const look = (house) => {
2330
  const [dx] = s.doorPoint(house);
2331
  s.prince.x = s.prince.prevX = dx - 300;
@@ -2353,18 +2295,20 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
2353
  const forge = s.houses[0];
2354
  const home = s.houses[3];
2355
 
2356
- // 1. THE LETTER IS ON THE NAIL, and it is the only thing there. A house
2357
- // he has not read has nowhere to put anything.
2358
- const withLetter = look(forge);
2359
- const letterLum = brightness(withLetter);
2360
- const noise = differs(look(forge), withLetter);
2361
-
2362
- // 2. TAKE IT, AND WHAT IS LEFT IS THE PATCH.
2363
- drive(0.2, { pickup: true });
2364
- drive(0.15);
2365
  const patch = look(forge);
2366
  const patchLum = brightness(patch);
2367
- const appeared = differs(patch, withLetter);
 
 
 
 
 
 
 
 
 
2368
 
2369
  // THE WALL BELOW IT, which is what the patch has to beat. Finding
2370
  // somewhere that is actually wall took three goes as the spot moved:
@@ -2407,13 +2351,9 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
2407
  const [hx] = s.doorPoint(home);
2408
  s.prince.x = s.prince.prevX = hx - 300;
2409
  drive(1.4, { torch: true });
2410
- drive(0.2, { pickup: true });
2411
- drive(0.15);
2412
  // Its own grain floor, measured here rather than borrowed from the
2413
- // forge: two looks at this door with nothing changed between them. The
2414
- // forge's floor was taken before its letter came off and under a
2415
- // different stretch of world, and a floor from somewhere else is not a
2416
- // floor.
2417
  const homeBefore = look(home);
2418
  const homeNoise = differs(look(home), homeBefore);
2419
  const homeSettled = look(home);
@@ -2422,14 +2362,14 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
2422
 
2423
  return {
2424
  name: 'the room asks',
2425
- pass: appeared > Math.max(300, noise * 4) &&
2426
  readsAsWall && patchLum > wall + 40 &&
2427
  filledDiff > Math.max(300, noise * 4) &&
2428
  homeDiff <= Math.max(homeNoise * 2, 200),
2429
  letterOnTheNailLum: letterLum,
2430
  patchLum, wallLum: wall, doorwayLum: doorway,
2431
  grainNoiseFloor: noise, atTheFifthNail: homeNoise,
2432
- takingTheLetterRevealsTheSlot: appeared > Math.max(300, noise * 4),
2433
  andTheSlotIsAMarkNotASocket: readsAsWall,
2434
  itReadsAgainstTheWall: patchLum > wall + 40,
2435
  pinningPutsItOnTheWall: filledDiff > Math.max(300, noise * 4),
@@ -2517,20 +2457,18 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
2517
  };
2518
  const atInit = isFirst(false) === true;
2519
 
2520
- // It survives a letter, four fragments and a key going on top of it.
2521
- for (let k = 0; k < 5; k++) {
2522
- s.prince.x = s.prince.prevX = s.houses[k].x - 300;
2523
- s.camera.reset(s.prince.x);
2524
- drive(1.2, { torch: true });
2525
- drive(0.4, { torch: true, pickup: true });
2526
- }
2527
  const afterTaking = isFirst(false) === true;
2528
 
2529
- // It survives pinning and retrieving at a patch.
 
2530
  const armedPages = s.sheaf.pages.length;
2531
- s.sheaf.index = s.sheaf.pages.findIndex((p) => p.kind === 'fragment');
2532
- const h = s.houses.find((hh) => hh.letterTaken) ?? s.houses[0];
2533
- s.prince.x = s.prince.prevX = h.x + (s.letterPoint(h)?.[0] ?? h.x) - h.x;
 
 
2534
  drive(0.3, { pickup: true });
2535
  drive(0.3);
2536
  drive(0.3, { pickup: true });
@@ -2611,8 +2549,15 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
2611
  const frozen = Math.abs(s.prince.x - from) < 0.001 && !s.prince.attacking;
2612
 
2613
  // The page goes down on the existing ramp and he can walk.
 
 
 
 
 
 
2614
  drive(0.8);
2615
- const lowered = !s.sheaf.up && s.torch.raise < 0.01 && s.band.alpha < 0.01;
 
2616
  drive(0.5, { right: true });
2617
  const controlsLive = s.prince.x > from + 100;
2618
 
@@ -2821,10 +2766,11 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
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();
@@ -2836,46 +2782,564 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
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
 
@@ -3274,7 +3738,9 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
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
 
@@ -3309,6 +3775,8 @@ export function createChecks({ renderer, scene, canvas, clips = {}, audio = null
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)]));
 
14
  // continuous tone allowed in the background the proxy stops working, so the two
15
  // tests below ask the real question directly, in the two places it matters.
16
 
17
+ import { TUNING, BOWL, TORCH, REVEAL_T } from './tuning.js';
18
  import { HOUSE_OF, laneLook, figureMidY } from './drained.js';
19
  import { BAND, linesNeeded, bandBox } from './band.js';
20
  import { GROUND_Y, W, H } from './scene.js';
 
715
  /**
716
  * THE SHEAF. A pile of loose paper, and every claim here is about paper.
717
  *
718
+ * His orders are page one. Everything after them is in the order he picked
719
+ * it up β€” no grouping, no sorting, no index and no tabs, because all of those
720
+ * are the game telling him what he has, and what he has is the thing he is
721
+ * meant to work out.
722
+ *
723
+ * AND NOTHING IN IT CAME OFF A WALL. Letters stay on their doors and the
724
+ * note stays on its tree: he does not pocket a stranger's mail. So the paper
725
+ * in it is his orders and the fragments he took off bodies, and in village
726
+ * one that is five pages at the most. Keys go in too, and come out again at
727
+ * the gate; they are carried, and they are not paper.
728
  */
729
  function sheafPages() {
730
  scene.reset();
731
  const sheaf = s.sheaf;
732
+ const isPaper = (p) => p.kind === 'letter' || p.kind === 'fragment';
733
 
734
  // 1. IT STARTS WITH HIS ORDERS AND NOTHING ELSE.
735
  //
 
743
  order.kind === 'letter' && !s.houses[order.house] &&
744
  s.textFor(order) === s.letters.byHouse[order.house].letter;
745
 
746
+ // 2. LETTERS STAY WHERE THEY WERE LEFT. Light every door and the tree,
747
+ // stand at each and press E: the band reads it, and the pile does not
748
+ // grow by a single page. With his orders on top, E at a bare slot does
749
+ // nothing at all.
750
+ const stops = s.houses.map((h) => ({ x: h.x, text: s.letters.byHouse[h.index].letter }));
751
+ if (s.tree) stops.push({ x: s.tree.x, text: s.letters.byHouse[s.tree.index].letter });
752
+ let readEveryOne = true;
753
+ for (const st of stops) {
754
+ s.prince.x = s.prince.prevX = st.x - 300;
755
+ s.prince.facing = 1;
756
+ s.camera.reset(s.prince.x);
757
+ drive(1.4, { torch: true });
758
+ readEveryOne = readEveryOne && s.band.text === st.text;
759
+ tap();
760
+ }
761
+ const nothingCameOffAWall = sheaf.pages.length === 1 && sheaf.pages[0] === order;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
762
 
763
  // 3. And running off the front of a one-page pile stays on that page.
764
  drive(0.5, { sheaf: true, left: true });
765
  const holdsAtTheFront = sheaf.index === 0 && !!s.band.text;
766
 
767
+ // 4. HIS ORDERS AND FOUR FRAGMENTS, AND THAT IS ALL THE PAPER THERE IS.
768
+ // armed() downs every carrier in the village and takes what each one
769
+ // was carrying, in the order they stand β€” so this is the fullest the
770
+ // pile's paper can ever be, in the order he found it.
771
+ const carriers = armed();
772
+ const paper = sheaf.pages.filter(isPaper);
773
+ const fullPile = paper.length === 5 && paper.length === carriers.length + 1 &&
774
+ paper[0].kind === 'letter' && !s.houses[paper[0].house] &&
775
+ paper.slice(1).every((p) => p.kind === 'fragment');
776
+ const neverAWallsLetter = sheaf.pages.every((p) => p.kind !== 'letter' ||
777
+ (!s.houses[p.house] && p.house !== s.tree?.index));
778
+ const foundOrder = paper.slice(1).map((p) => p.kept).join(',');
779
+ const takenOrder = carriers.map((c) => c.kept).join(',');
780
+
781
+ // 5. A FLIP WITH A BEAT. Holding an arrow walks the pile at reading speed,
782
+ // not at frame rate. Hold right for a second from the front and count
783
+ // the turns: at 60fps a free browse would be sixty.
784
  sheaf.index = 0;
785
  const before = sheaf.turns;
786
  drive(1, { sheaf: true, right: true });
787
  const turnsPerSecond = sheaf.turns - before;
788
 
789
+ // 6. IT DOES NOT WRAP. Running off the end of a pile of paper does nothing.
790
  drive(4, { sheaf: true, right: true });
791
  const stopsAtTheEnd = sheaf.index === sheaf.pages.length - 1;
792
  drive(6, { sheaf: true, left: true });
793
  const stopsAtTheStart = sheaf.index === 0;
794
 
795
+ // 7. THE INDEX SURVIVES RELEASE. He reopens where he left off.
796
  drive(1.2, { sheaf: true, right: true });
797
  const wasOn = sheaf.index;
798
  drive(1); // sheaf down, walking about
799
  drive(0.6, { sheaf: true });
800
  const reopensWhereHeLeftIt = sheaf.index === wasOn && wasOn > 0;
801
 
802
+ // 8. A `?` RESOLVES THE MOMENT HE PAGES TO IT, and only the one he turned
803
  // to. Two blind takes, open the second: it fills, the first stays blank.
804
  // The pages BEHIND the one in his hand are what the unknown state is for.
805
  scene.reset();
 
813
  s.band.text === s.letters.byHouse[2].fragment;
814
  const resolvedTheOneHeIsOn = !s.prince.inventory[0].seen;
815
 
816
+ // 9. And the one still behind him reads as blank paper β€” no words in the
817
  // band for a page he has not turned to.
818
  sheaf.index = 1; // the FIRST fragment, past the order
819
  s.band.text = null;
 
824
 
825
  return {
826
  name: 'the sheaf pages',
827
+ pass: startsWithTheOrder && readEveryOne && nothingCameOffAWall &&
828
+ holdsAtTheFront && fullPile && neverAWallsLetter &&
829
+ foundOrder === takenOrder &&
830
+ turnsPerSecond >= 3 && turnsPerSecond <= 5 &&
831
  stopsAtTheEnd && stopsAtTheStart && reopensWhereHeLeftIt &&
832
  blankWhileUnknown && resolvedOnPage && resolvedTheOneHeIsOn &&
833
  opensWhenHeTurnsToIt,
834
+ startsWithTheOrder,
835
+ readsEveryDoorAndTheTree: readEveryOne,
836
+ andTakesNothingOffAWall: nothingCameOffAWall,
 
 
 
837
  holdsAtTheFrontOfThePile: holdsAtTheFront,
838
+ paperAtItsFullest: paper.length, ordersAndFourFragments: fullPile,
839
+ neverAHouseLetterOrTheNote: neverAWallsLetter,
840
+ foundOrder, takenOrder, turnsPerSecondHeld: turnsPerSecond,
841
  stopsAtTheEnd, stopsAtTheStart, reopensWhereHeLeftIt,
842
  blankUntilHeTurnsToIt: blankWhileUnknown, resolvesOnPage: resolvedOnPage,
843
  leavesThePagesBehindHimBlank: resolvedTheOneHeIsOn,
 
1017
  ticks++;
1018
  }
1019
  drive(1.4, { torch: true });
1020
+ // HIS ORDERS ON TOP, so the press can only be for the key. It used to go
1021
+ // to the letter first; letters stay on the door now, and a fragment on
1022
+ // top at a door that is not its own would pin there and wake somebody.
1023
+ s.sheaf.index = 0;
1024
+ tap();
1025
  keyRows.push({ house: s.houses[k].kind, at: Math.round(kx),
1026
  open: s.houses[k].open, taken: s.prince.keys > before });
1027
  }
 
1036
  s.camera.reset(s.prince.x);
1037
  drive(1.6, { torch: true });
1038
  const heldBefore = s.prince.keys;
1039
+ s.sheaf.index = 0; // his orders on top: nothing to pin here
1040
  tap(); tap(); tap();
1041
  const residue = { lit: loom.seen, open: loom.open, taken: loom.keyTaken,
1042
  gotNothing: s.prince.keys === heldBefore };
 
1197
  /**
1198
  * Stand at house i with the sheaf up, on the page for fragment k.
1199
  *
1200
+ * THE DOOR IS LIT FIRST, the one time it needs to be: the slot takes a pin
1201
+ * from the first light, and the letter stays where it is.
 
1202
  */
1203
  function atDoor(i, k = null) {
1204
  s.prince.x = s.prince.prevX = s.houses[i].x - 300;
1205
  s.prince.facing = 1;
1206
  s.camera.reset(s.prince.x);
1207
+ if (!s.houses[i].seen) drive(1.4, { torch: true });
 
 
 
 
1208
  if (k !== null) {
1209
  s.sheaf.index = s.sheaf.pages.findIndex((p) => p.kind === 'fragment' && p.kept === k);
1210
  }
 
1235
  drive(0.6); // Q released
1236
  const stillFragment = held()?.kind === 'fragment' && !s.sheaf.up;
1237
 
1238
+ // A letter is a different shape in the hand, and it can be on top too.
1239
+ // The only one he carries is his orders β€” the rest stay on their doors.
 
 
 
 
 
 
1240
  const li = s.sheaf.pages.findIndex((p) => p.kind === 'letter');
1241
  s.sheaf.index = li;
1242
  drive(0.6);
 
1309
  const A = 0, B = 2;
1310
  const h = s.houses[H];
1311
 
1312
+ // The door has to have been looked at once for anything to be offered,
1313
+ // which is the latch, not a mode. THERE IS NO LETTER ROW ANY MORE: it
1314
+ // took the letter, and letters stay on the door.
 
 
1315
  stand(H, withQ);
1316
+ drive(1.4, { torch: true });
 
1317
 
1318
+ // 2. A bare slot, a fragment on top -> pins it.
1319
  put(A); stand(H, withQ); press();
1320
  const pinned = h.filed === A;
1321
 
 
1343
  const forge = s.houses[F];
1344
  put(0); stand(F, withQ);
1345
  drive(1.4, { torch: true });
 
1346
  put(0); stand(F, withQ); press(); // the forge's own, so it settles
1347
  const settled = forge.settled && forge.filed === 0;
1348
  put(B); stand(F, withQ); press(); // try to swap it
1349
  const lockHeld = forge.filed === 0 && settled;
1350
 
1351
+ rows.push({ withQ, pinned, swapped, tookBackSame,
1352
  tookBackWithLetter, bareAndLetterDoesNothing, lockHeld });
1353
  }
1354
  scene.reset();
 
1396
  return withOverlayOff(() => {
1397
  const clip = s.houseClip;
1398
  const patch = (h) => {
1399
+ const [nx, ny, nw, nh] = clip.slotAt[h.variant];
1400
  return [Math.round(h.x - s.camera.x - clip.pivotX + nx),
1401
  Math.round(GROUND_Y - clip.pivotY + ny), nw, nh];
1402
  };
 
1446
  s.prince.facing = 1;
1447
  s.camera.reset(s.prince.x);
1448
  drive(1.4, { torch: true });
 
 
 
 
1449
  drive(0.5, { torch: true }); // settled, and staying up
1450
  };
1451
  const put = (k) => {
 
1458
  const forge = s.houses[0];
1459
  stand(forge);
1460
 
1461
+ // 1. HIS ORDERS ON TOP IS A BARE SLOT, and two renders of it back to back
1462
  // are the grain floor everything else is measured against.
1463
  put(null);
1464
  const bare = brightness(shot(forge));
1465
  const noise = Math.abs(brightness(shot(forge)) - bare);
1466
 
1467
+ // 1b. AND AN EMPTY SLOT DOES NOT GLOW. THIS CLAIM IS INVERTED.
1468
  //
1469
+ // It used to breathe, with the same pulse a dropped scrap has: the
1470
+ // patch only appeared once the letter came off, and nothing walked a
1471
+ // player over to find it. The slot is under the letter from the first
1472
+ // light now, and a bare nail under a letter asks its own question. A
1473
+ // pulse on a wall reads as UI β€” and the loom house's empty slot has to
1474
+ // read as a letter nobody answered, all level, not as a button.
1475
  //
1476
  // Measured over one full cycle of KEPT_PULSE_HZ, on a box wider than
1477
+ // the slot so any halo around it would be counted too.
1478
  const wide = (h) => {
1479
+ const [nx, ny, nw, nh] = clip.slotAt[h.variant];
1480
  return [Math.round(h.x - s.camera.x - clip.pivotX + nx) - 26,
1481
  Math.round(GROUND_Y - clip.pivotY + ny) - 26, nw + 52, nh + 52];
1482
  };
 
1490
  s.clock = 3.0;
1491
  return Math.round(Math.max(...seen) - Math.min(...seen));
1492
  };
1493
+ const emptyIsStill = breath(forge);
1494
 
1495
  // 2. A SCRAP ON TOP AND THE WALL SHOWS IT.
1496
  put(0);
 
1508
  const pinned = brightness(shot(forge));
1509
  const ghostIsNotThePin = ghosted - pinned; // solid, so darker again
1510
 
1511
+ // AND A FULL ONE IS STILL TOO. Nothing on this wall pulses, ever.
 
 
 
1512
  const filledIsStill = breath(forge);
1513
 
1514
  // 4. AND IT IS OVER. Lightning, not a filter.
 
1533
  pass: ghostShows > Math.max(8, noise * 3) &&
1534
  ghostIsNotThePin > Math.max(8, noise * 3) &&
1535
  wrongGhost > Math.max(8, noise * 3) &&
1536
+ emptyIsStill <= 2 && filledIsStill <= 2 &&
1537
  struck && quiet && gone,
1538
  flickerMovesTheMeanBy: noise,
1539
+ anEmptySlotMovesBy: emptyIsStill,
1540
+ andAFilledOneBy: filledIsStill,
1541
  theWallPreviewsTheScrap: ghostShows,
1542
  andItPreviewsAWrongOneToo: wrongGhost,
1543
  theGhostIsNotThePin: ghostIsNotThePin,
 
1692
  const visible = rows.every((r) => r.ink > 120 && r.light > 200 && r.litByTheTorch);
1693
  const noneOfferedWhileShut = rows.every((r) => !r.offered);
1694
 
1695
+ // THE DOOR CARRIES FOUR THINGS NOW β€” letter, slot, key and room β€” and
1696
+ // whether they all fit is its own check. See theDoorCarriesFour.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1697
 
1698
  // 2. NOT HIS WHILE THE DOOR IS SHUT.
1699
+ // HIS ORDERS ON TOP. The top page after armed() is a fragment, and E at
1700
+ // a door that is not its own pins it there β€” and wakes its owner.
1701
  s.prince.x = s.prince.prevX = s.houses[0].x - 300;
1702
  drive(1.6, { torch: true });
1703
+ s.sheaf.index = 0;
1704
  tap(); tap();
1705
  const shut = { keys: s.prince.keys, taken: s.houses[0].keyTaken };
1706
 
 
1714
  s.prince.x = s.prince.prevX = s.houses[k].x - 300;
1715
  s.camera.reset(s.prince.x);
1716
  drive(1.6, { torch: true });
1717
+ s.sheaf.index = 0; // his orders on top: the press is for the key
1718
+ tap();
1719
  }
1720
  const open = { keys: s.prince.keys, taken: s.houses.map((h) => h.keyTaken) };
1721
 
 
1727
  s.camera.reset(s.prince.x);
1728
  drive(1.6, { torch: true });
1729
  const offeredAtTheLoom = s.canTakeKey;
1730
+ s.sheaf.index = 0;
1731
  tap(); tap(); tap();
1732
  const residue = { open: s.houses[3].open, taken: s.houses[3].keyTaken,
1733
  offered: offeredAtTheLoom, keys: s.prince.keys };
1734
 
1735
  return {
1736
  name: 'the key is behind the door',
1737
+ pass: visible && noneOfferedWhileShut &&
1738
  shut.keys === 0 && !shut.taken &&
1739
  open.keys === 4 && open.taken.filter(Boolean).length === 4 &&
1740
  !residue.open && !residue.taken && !residue.offered && residue.keys === 4 &&
1741
  carriers.length === 4,
1742
  visibleInEveryLitDoorway: visible,
1743
  andNotOfferedWhileShut: noneOfferedWhileShut,
 
 
1744
  shut, open,
1745
  theLoomHouseNeverOpens: residue,
1746
  rows,
 
1909
  */
1910
  function markingIsAClaim() {
1911
  scene.reset();
1912
+ // Stand at house i's door with the sheaf up, the door lit.
 
 
1913
  const at = (i) => atDoor(i);
1914
  const mark = () => drive(0.3, { sheaf: true, pickup: true });
1915
  const filed = () => s.houses.map((h) => h.filed);
 
1928
  // nail is still bare.
1929
  const nothingAutoFiles = filed().every((f) => f == null);
1930
 
1931
+ // 1b. AND A DOOR TAKES A PIN THE FIRST TIME IT IS LIT. THIS CLAIM IS
1932
+ // INVERTED.
1933
  //
1934
+ // It used to assert that there was nowhere to file until the letter
1935
+ // came off the nail: one spot, the letter on it, and the patch it left
1936
+ // was the slot. Letters stay on the door now and the slot is under the
1937
+ // letter from the first light, so a door he has only just lit takes a
1938
+ // pin like any other. It is the MOVE in claim 2 that takes it off again.
1939
  const unread = s.houses[2];
1940
+ const neverLit = !unread.seen;
1941
  s.prince.x = s.prince.prevX = unread.x - 300;
1942
  s.prince.facing = 1;
1943
  s.camera.reset(s.prince.x);
1944
  s.sheaf.index = fragA;
1945
  drive(1.2, { sheaf: true });
1946
+ const offeredOnFirstLight = unread.seen && s.canMark && s.canPin;
 
1947
  mark();
1948
+ const slotFromTheFirstLight = neverLit && offeredOnFirstLight && unread.filed === 2;
 
 
 
 
 
 
1949
 
1950
  // 2. He files one at a lit door.
1951
  s.sheaf.index = fragA; at(0); mark();
 
2023
 
2024
  return {
2025
  name: 'marking is a claim',
2026
+ pass: nothingAutoFiles && slotFromTheFirstLight && noSheafNeeded &&
2027
  afterFirst[0] === 2 && afterReplace[0] === 3 &&
2028
  afterMove[0] == null && afterMove[1] === 3 && onlyOnce &&
2029
  needsADoor && mapCannotBeFiled && noVerdict &&
2030
  frozenAtTheGate,
2031
  nothingFilesItself: nothingAutoFiles,
2032
+ aDoorTakesAPinTheFirstTimeItIsLit: slotFromTheFirstLight,
 
2033
  afterFirst, afterReplace, afterMove,
2034
  oneFragmentOneHouse: onlyOnce,
2035
  needsALitDoor: needsADoor,
 
2246
  * at a door, which is the thing the cue was meant to teach. So the question
2247
  * is asked in the world.
2248
  *
2249
+ * IT WAS A FRAME AND IT IS NOW TWO NAILS. A bright square box bolted to every
2250
  * house read as a noticeboard, and no village in any century had one. A
2251
  * letter nailed up is how you reached somebody who was not home, anywhere,
2252
+ * ever β€” and under it hangs a second nail and the lighter patch of wall where
2253
+ * their own reply hung. That patch is the slot. The frame was drawn; the
2254
+ * patch is a VALUE, and it is there from the first light, beside a letter
2255
+ * that never comes down.
2256
  *
2257
  * THE GATE HAS ALWAYS WORKED THIS WAY. Four empty sockets on the leaf, and
2258
  * nobody has ever needed telling what they are for.
 
2260
  * Measured in pixels, because it is entirely a claim about what is on screen.
2261
  */
2262
  function theRoomAsks() {
2263
+ const onScreen = (house, rect) => {
2264
  const clip = s.houseClip;
2265
+ const [fx, fy, fw, fh] = rect;
2266
  return [Math.round(house.x - s.camera.x - clip.pivotX + fx),
2267
  Math.round(GROUND_Y - clip.pivotY + fy), fw, fh];
2268
  };
2269
+ const nailRect = (house) => onScreen(house, s.houseClip.slotAt[house.variant]);
2270
+ const letterRect = (house) => onScreen(house, s.houseClip.letterAt[house.variant]);
2271
  const look = (house) => {
2272
  const [dx] = s.doorPoint(house);
2273
  s.prince.x = s.prince.prevX = dx - 300;
 
2295
  const forge = s.houses[0];
2296
  const home = s.houses[3];
2297
 
2298
+ // 1. THE FIRST LIGHT, AND BOTH ARE THERE. Nothing has been read, taken or
2299
+ // pinned: the letter hangs on its nail and the slot is under it.
 
 
 
 
 
 
 
2300
  const patch = look(forge);
2301
  const patchLum = brightness(patch);
2302
+ const noise = differs(look(forge), patch);
2303
+ const letterLum = (() => {
2304
+ const [x, y, w, h] = letterRect(forge);
2305
+ return brightness(ctx.getImageData(x, y, w, h).data);
2306
+ })();
2307
+
2308
+ // 2. AND THE LETTER IS PAPER, NOT PATCH. The two share a column and a
2309
+ // shape, so the thing that tells them apart is the value: the letter
2310
+ // is a lit page with writing on it, the slot is a paler mark on wood.
2311
+ const letterIsPaper = letterLum > patchLum + 20;
2312
 
2313
  // THE WALL BELOW IT, which is what the patch has to beat. Finding
2314
  // somewhere that is actually wall took three goes as the spot moved:
 
2351
  const [hx] = s.doorPoint(home);
2352
  s.prince.x = s.prince.prevX = hx - 300;
2353
  drive(1.4, { torch: true });
 
 
2354
  // Its own grain floor, measured here rather than borrowed from the
2355
+ // forge: two looks at this door with nothing changed between them. A
2356
+ // floor taken under a different stretch of world is not a floor.
 
 
2357
  const homeBefore = look(home);
2358
  const homeNoise = differs(look(home), homeBefore);
2359
  const homeSettled = look(home);
 
2362
 
2363
  return {
2364
  name: 'the room asks',
2365
+ pass: letterIsPaper &&
2366
  readsAsWall && patchLum > wall + 40 &&
2367
  filledDiff > Math.max(300, noise * 4) &&
2368
  homeDiff <= Math.max(homeNoise * 2, 200),
2369
  letterOnTheNailLum: letterLum,
2370
  patchLum, wallLum: wall, doorwayLum: doorway,
2371
  grainNoiseFloor: noise, atTheFifthNail: homeNoise,
2372
+ letterAndSlotBothThereFromTheFirstLight: letterIsPaper && readsAsWall,
2373
  andTheSlotIsAMarkNotASocket: readsAsWall,
2374
  itReadsAgainstTheWall: patchLum > wall + 40,
2375
  pinningPutsItOnTheWall: filledDiff > Math.max(300, noise * 4),
 
2457
  };
2458
  const atInit = isFirst(false) === true;
2459
 
2460
+ // It survives four fragments going on top of it.
2461
+ armed();
 
 
 
 
 
2462
  const afterTaking = isFirst(false) === true;
2463
 
2464
+ // It survives pinning and retrieving at a slot. Not the forge's own
2465
+ // fragment, or it settles and the retrieve is refused.
2466
  const armedPages = s.sheaf.pages.length;
2467
+ const h = s.houses[0];
2468
+ s.sheaf.index = s.sheaf.pages.findIndex((p) => p.kind === 'fragment' && p.kept !== h.index);
2469
+ s.prince.x = s.prince.prevX = h.x - 300;
2470
+ s.camera.reset(s.prince.x);
2471
+ if (!h.seen) drive(1.4, { torch: true });
2472
  drive(0.3, { pickup: true });
2473
  drive(0.3);
2474
  drive(0.3, { pickup: true });
 
2549
  const frozen = Math.abs(s.prince.x - from) < 0.001 && !s.prince.attacking;
2550
 
2551
  // The page goes down on the existing ramp and he can walk.
2552
+ //
2553
+ // THE BAND STOPS READING HIS ORDERS, which is not the same as closing. At
2554
+ // ?at=gate he starts 320px from the gatehouse, and reading his orders by
2555
+ // torchlight lights its letter too β€” so with the orders down and him stood
2556
+ // still, the band reads that door. That is the rule for a lit door, not
2557
+ // the order hanging on.
2558
  drive(0.8);
2559
+ const lowered = !s.sheaf.up && s.torch.raise < 0.01 &&
2560
+ (s.band.alpha < 0.01 || s.band.text !== s.letters.byHouse[oh].letter);
2561
  drive(0.5, { right: true });
2562
  const controlsLive = s.prince.x > from + 100;
2563
 
 
2766
  * read before the first fight β€” and far enough from every drained that
2767
  * lighting it wakes nobody.
2768
  *
2769
+ * It reads exactly the way a door does. Light it and the band reads it;
2770
+ * after that, standing by it reads it again with the torch down; walking
2771
+ * past, it does not. It STAYS ON THE TREE, like every letter stays on its
2772
+ * door, so it is never a page. And it is not a door: holding a fragment
2773
+ * there, nothing is offered, and E does nothing.
2774
  */
2775
  function theTreeTeaches() {
2776
  scene.reset();
 
2782
  const clearOfAll = Math.min(...road.map((d) => Math.abs(d.x - t.x)));
2783
  const placed = t.x > first + TUNING.TREE_CLEAR && t.x < BOWL.X0 &&
2784
  clearOfAll >= TUNING.TREE_CLEAR;
2785
+ const settle = TUNING.READ_STILL_MS / 1000 + 0.4;
2786
 
2787
+ // Up to it in the dark, and stood there: nothing read. The first read
2788
+ // needs light.
2789
  s.prince.x = s.prince.prevX = t.x - 300;
2790
  s.prince.facing = 1;
2791
  s.camera.reset(s.prince.x);
2792
+ drive(settle);
2793
+ const unreadInTheDark = !t.seen && s.band?.text !== note;
2794
 
2795
  // Lit: the band reads it, and nobody wakes.
2796
  drive(1.4, { torch: true });
2797
+ const reads = t.seen && s.band?.text === note;
2798
  const nobodyWoke = s.drained.every((d) => !d.revealed);
2799
 
2800
+ // Torch down and still stood by it: it reads again, for nothing.
2801
+ drive(settle);
2802
+ const readsAgain = s.torch.raise < 0.01 && s.band?.text === note;
2803
+
2804
+ // Walking past it, it does not. Every frame of a walk from one side of
2805
+ // its reach to the other.
2806
+ let openWhileWalking = 0;
2807
+ let frames = 0;
2808
+ while (s.prince.x < t.x + TUNING.DOOR_REACH + 100 && frames < 60 * 20) {
2809
+ scene.update(1 / 60, { ...NONE, right: true });
2810
+ if (s.band?.text) openWhileWalking++;
2811
+ frames++;
2812
+ }
2813
+ const closedWhileWalking = openWhileWalking === 0 && frames > 0;
2814
 
2815
+ // It stays on the tree: E there does nothing, holding a fragment or not,
2816
+ // and the pile is what it was.
2817
+ s.prince.x = s.prince.prevX = t.x - 300;
2818
+ s.camera.reset(s.prince.x);
2819
  s.prince.inventory.push({ kept: 0, answers: null, seen: true });
2820
  s.sheaf.pages.push({ kind: 'fragment', kept: 0 });
2821
  s.sheaf.index = s.sheaf.pages.length - 1;
2822
+ const pile = s.sheaf.pages.length;
2823
  drive(0.5, { torch: true });
2824
  drive(0.3);
2825
+ const nothingGoesBack = !s.canPin && !s.canMark;
2826
+ tap();
2827
+ s.sheaf.index = 0;
2828
+ tap();
2829
+ const staysOnTheTree = s.sheaf.pages.length === pile &&
2830
+ s.sheaf.pages.every((p) => p.kind !== 'letter' || p.house !== t.index);
2831
+ const stillNobodyWoke = s.drained.every((d) => !d.revealed);
2832
 
2833
  scene.reset();
2834
+ const unlitAfterARestart = !t.seen;
2835
  return {
2836
  name: 'the tree teaches',
2837
+ pass: !!note && placed && unreadInTheDark && reads && nobodyWoke &&
2838
+ readsAgain && closedWhileWalking && nothingGoesBack && staysOnTheTree &&
2839
+ stillNobodyWoke && unlitAfterARestart,
2840
  at: t.x, pastTheFirstDrained: t.x - first, clearOfEveryDrained: clearOfAll,
2841
  clearFloor: TUNING.TREE_CLEAR, beforeTheVillage: t.x < BOWL.X0,
2842
  unreadInTheDark, readsUnderTheTorch: reads, nobodyWoke,
2843
+ readsAgainWithTheTorchDown: readsAgain,
2844
+ framesOpenWhileWalkingPast: openWhileWalking, walkedFrames: frames,
2845
+ nothingGoesBack, staysOnTheTree, stillNobodyWoke, unlitAfterARestart,
2846
+ };
2847
+ }
2848
+
2849
+ // --- letters stay on the door ------------------------------------------------
2850
+ //
2851
+ // Letters stay where they were left; fragments come off bodies. He does not
2852
+ // pocket a stranger's mail, and the door becomes where he judges: the
2853
+ // question nailed in front of him, the answer in his hand. See
2854
+ // docs/specs/astara-letters-stay-addendum.md.
2855
+
2856
+ /**
2857
+ * THE LETTERS STAY, on every door and on the tree, whatever he does.
2858
+ *
2859
+ * There is no state for it any more β€” nothing can take one down β€” so this
2860
+ * looks at the wall. At the start of a run, after every input there is at
2861
+ * every door, and at the end of a whole run with the gate open: the letter's
2862
+ * rect is paper. And not once in all of that does a letter from a wall turn
2863
+ * up in the sheaf.
2864
+ *
2865
+ * PAPER IS COUNTED, NOT AVERAGED. The share of the rect brighter than 140,
2866
+ * which nothing on a wall at night comes near except a lit page β€” and two
2867
+ * renders of the same frame are the grain floor it has to clear.
2868
+ */
2869
+ function lettersStay() {
2870
+ return withOverlayOff(() => {
2871
+ const orderAt = orderHouse();
2872
+ let wallLettersHeld = 0;
2873
+ const note = () => {
2874
+ wallLettersHeld += s.sheaf.pages
2875
+ .filter((p) => p.kind === 'letter' && p.house !== orderAt).length;
2876
+ };
2877
+ const targets = [...s.houses, ...(s.tree ? [s.tree] : [])];
2878
+ const clipOf = (t) => (t === s.tree ? s.treeClip : s.houseClip);
2879
+ const rectOf = (t) => clipOf(t).letterAt[t.variant];
2880
+ // WHERE HE STANDS: 130px short of the letter, which is h.x - 300 at a
2881
+ // house β€” where every check and every player reads a door from β€” and
2882
+ // the same distance from the note on the tree.
2883
+ const standX = (t) => {
2884
+ const r = rectOf(t);
2885
+ return t.x - clipOf(t).pivotX + r[0] + r[2] / 2 - 130;
2886
+ };
2887
+ const paper = (t) => {
2888
+ const clip = clipOf(t);
2889
+ const [rx, ry, rw, rh] = rectOf(t);
2890
+ const d = ctx.getImageData(Math.round(t.x - s.camera.x - clip.pivotX + rx),
2891
+ Math.round(GROUND_Y - clip.pivotY + ry), rw, rh).data;
2892
+ let n = 0;
2893
+ for (let i = 0; i < d.length; i += 4) if (lum(d, i) > 140) n++;
2894
+ return n / (d.length / 4);
2895
+ };
2896
+ const shot = () => {
2897
+ s.clock = 3.0;
2898
+ renderer.drawLevel(scene, 0, { pass1Ms: 0, pass2Ms: 0, bgDrawn: 0 });
2899
+ };
2900
+ const look = (t) => {
2901
+ s.prince.x = s.prince.prevX = standX(t);
2902
+ s.prince.facing = 1;
2903
+ s.camera.reset(s.prince.x);
2904
+ drive(1.4, { torch: true });
2905
+ shot();
2906
+ const a = paper(t);
2907
+ shot();
2908
+ const floor = Math.abs(paper(t) - a);
2909
+ note();
2910
+ return { at: t.kind, paper: +a.toFixed(3), floor: +floor.toFixed(3),
2911
+ present: a > Math.max(0.35, floor * 4) };
2912
+ };
2913
+
2914
+ // 1. FROM THE FIRST FRAME. Every carrier down and every fragment taken,
2915
+ // and every letter is still where it was left.
2916
+ const carriers = armed();
2917
+ const atStart = targets.map(look);
2918
+
2919
+ // 2. AND NOTHING HE CAN DO AT A DOOR TAKES ONE DOWN. E with his orders
2920
+ // on top, Q, SPACE, F β€” and E with the house's own fragment, which
2921
+ // pins it and sends its owner home.
2922
+ const afterInputs = targets.map((t) => {
2923
+ look(t);
2924
+ s.sheaf.index = 0; tap();
2925
+ drive(0.6, { sheaf: true }); drive(0.3);
2926
+ drive(0.6, { torch: true }); drive(0.3);
2927
+ drive(0.1, { attack: true }); drive(0.6);
2928
+ const own = t === s.tree ? -1
2929
+ : s.sheaf.pages.findIndex((p) => p.kind === 'fragment' && p.kept === t.index);
2930
+ if (own >= 0) { s.sheaf.index = own; tap(); }
2931
+ note();
2932
+ return look(t);
2933
+ });
2934
+
2935
+ // 3. A WHOLE RUN. Everybody who can go home does, four keys come off
2936
+ // four hooks, and the gate opens on them.
2937
+ let ticks = 0;
2938
+ while (s.drained.some((d) => d.homing) && ticks < 60 * 250) {
2939
+ scene.update(1 / 60, NONE); ticks++;
2940
+ }
2941
+ for (const h of s.houses) {
2942
+ if (!h.open) continue;
2943
+ s.prince.x = s.prince.prevX = h.x - 300;
2944
+ s.camera.reset(s.prince.x);
2945
+ drive(1.4, { torch: true });
2946
+ s.sheaf.index = 0;
2947
+ tap();
2948
+ }
2949
+ note();
2950
+ s.prince.x = s.prince.prevX = s.gateStopX;
2951
+ s.camera.reset(s.prince.x);
2952
+ drive(0.3);
2953
+ for (let i = 0; i < TUNING.GATE_WANTS; i++) tap();
2954
+ drive(2 + TUNING.GATE_OPEN_MS / 1000);
2955
+ const gateOpen = s.gate.open >= 1;
2956
+ note();
2957
+ const atTheEnd = targets.map(look);
2958
+
2959
+ const all = (rows) => rows.every((r) => r.present);
2960
+ return {
2961
+ name: 'letters stay',
2962
+ pass: carriers.length === TUNING.GATE_WANTS && targets.length === 6 &&
2963
+ all(atStart) && all(afterInputs) && gateOpen && all(atTheEnd) &&
2964
+ wallLettersHeld === 0,
2965
+ onEveryWallAtTheStart: all(atStart),
2966
+ afterEveryInputAtEveryDoor: all(afterInputs),
2967
+ andAtTheEndOfARunWithTheGateOpen: gateOpen && all(atTheEnd),
2968
+ wallLettersEverInTheSheaf: wallLettersHeld,
2969
+ atStart, afterInputs, atTheEnd,
2970
+ };
2971
+ });
2972
+ }
2973
+
2974
+ /**
2975
+ * THE SLOT IS ALWAYS THERE, from the first light, at every door.
2976
+ *
2977
+ * It is the affordance, and it used to be earned: no slot until the letter
2978
+ * came off. Now a bare nail and a lighter patch sit under the letter from the
2979
+ * first moment the torch finds the wall β€” before anything is pinned, and
2980
+ * before anything is read. Rendered with the door's latch wiped, to prove
2981
+ * the wall draws it off nothing but the light.
2982
+ *
2983
+ * AND IT IS STILL. A value, not a glow: over a whole pulse cycle the patch
2984
+ * and the wall round it do not move. The tree has no slot at all.
2985
+ */
2986
+ function theSlotIsAlwaysThere() {
2987
+ return withOverlayOff(() => {
2988
+ const clip = s.houseClip;
2989
+ const box = (h, pad = 0) => {
2990
+ const [nx, ny, nw, nh] = clip.slotAt[h.variant];
2991
+ return [Math.round(h.x - s.camera.x - clip.pivotX + nx) - pad,
2992
+ Math.round(GROUND_Y - clip.pivotY + ny) - pad, nw + pad * 2, nh + pad * 2];
2993
+ };
2994
+ const brightness = (d) => {
2995
+ let sum = 0;
2996
+ for (let i = 0; i < d.length; i += 4) sum += lum(d, i);
2997
+ return Math.round(sum / (d.length / 4));
2998
+ };
2999
+ const render = (clock = 3.0) => {
3000
+ s.clock = clock;
3001
+ renderer.drawLevel(scene, 0, { pass1Ms: 0, pass2Ms: 0, bgDrawn: 0 });
3002
+ };
3003
+ const rows = s.houses.map((h) => {
3004
+ scene.reset();
3005
+ s.prince.x = s.prince.prevX = h.x - 300;
3006
+ s.prince.facing = 1;
3007
+ s.camera.reset(s.prince.x);
3008
+ drive(1.4, { torch: true });
3009
+ // FORGET HE EVER LOOKED. The render reads state only, so with the
3010
+ // latch wiped nothing in the scene says this door was lit or read.
3011
+ h.seen = false;
3012
+ render();
3013
+ const slot = brightness(ctx.getImageData(...box(h)).data);
3014
+ const [bx, by, bw, bh] = box(h);
3015
+ const wall = brightness(ctx.getImageData(bx, by + bh + 8, bw, bh).data);
3016
+ render();
3017
+ const floor = Math.abs(brightness(ctx.getImageData(...box(h)).data) - slot);
3018
+ const seenPulse = [];
3019
+ for (let i = 0; i < 12; i++) {
3020
+ render(3.0 + (i / 12) / TORCH.KEPT_PULSE_HZ);
3021
+ seenPulse.push(brightness(ctx.getImageData(...box(h, 26)).data));
3022
+ }
3023
+ render();
3024
+ return { house: h.kind, slot, wall, floor,
3025
+ movesOverAPulse: Math.max(...seenPulse) - Math.min(...seenPulse),
3026
+ nothingPinned: h.filed == null };
3027
+ });
3028
+ const everyDoor = rows.every((r) => r.nothingPinned &&
3029
+ r.slot > r.wall + 40 && r.slot - r.wall > r.floor * 3);
3030
+ const still = rows.every((r) => r.movesOverAPulse <= 2);
3031
+ const treeHasNone = !!s.treeClip && !s.treeClip.slotAt;
3032
+ scene.reset();
3033
+ return {
3034
+ name: 'the slot is always there',
3035
+ pass: everyDoor && still && treeHasNone,
3036
+ onEveryDoorFromTheFirstLight: everyDoor,
3037
+ andItDoesNotGlow: still,
3038
+ theTreeHasNone: treeHasNone,
3039
+ rows,
3040
+ };
3041
+ });
3042
+ }
3043
+
3044
+ /**
3045
+ * THE BAND READS THE DOOR, and it is one button to compare.
3046
+ *
3047
+ * Q held the page in his hand. Wins
3048
+ * the torch on a letter, in reach that letter. The first read
3049
+ * standing still, in reach of a lit door that door's letter
3050
+ * otherwise closed
3051
+ *
3052
+ * THE FIRST READ NEEDS LIGHT; EVERY ONE AFTER IS FREE. On a phone TORCH and
3053
+ * PAGES share a thumb, so comparing a door with the page in his hand cannot
3054
+ * need both: hold PAGES for the paper, let go for the door.
3055
+ *
3056
+ * And the band never reads one door while E would act on another. Sampled
3057
+ * on every frame this check runs.
3058
+ */
3059
+ function bandReadsTheDoor() {
3060
+ const wait = TUNING.READ_STILL_MS;
3061
+ const letterOf = (h) => s.letters.byHouse[h.index].letter;
3062
+ let frames = 0;
3063
+ let notTheDoorEActsOn = 0;
3064
+ const step = (input = {}) => {
3065
+ scene.update(1 / 60, { ...NONE, ...input });
3066
+ frames++;
3067
+ if (s.band.text) {
3068
+ const h = s.houses.find((hh) => letterOf(hh) === s.band.text);
3069
+ if (h && h !== s.nearDoor) notTheDoorEActsOn++;
3070
+ }
3071
+ };
3072
+ const run = (secs, input = {}, each = null) => {
3073
+ const n = Math.round(secs * 60);
3074
+ for (let i = 0; i < n; i++) { step(input); if (each) each(i); }
3075
+ };
3076
+ const standAt = (x) => {
3077
+ s.prince.x = s.prince.prevX = x;
3078
+ s.prince.facing = 1;
3079
+ s.camera.reset(x);
3080
+ };
3081
+
3082
+ scene.reset();
3083
+ const forge = s.houses[0];
3084
+ const L = letterOf(forge);
3085
+
3086
+ // 1. A DOOR HE HAS NEVER LIT: closed, however long he stands at it.
3087
+ standAt(forge.x - 300);
3088
+ run(wait / 1000 + 0.5);
3089
+ const neverLitIsClosed = !forge.seen && s.band.text == null;
3090
+
3091
+ // 2. THE FIRST READ IS THE TORCH'S. Walk in, stop, raise it: it reads
3092
+ // before standing still could have opened it.
3093
+ standAt(forge.x - 400);
3094
+ run(0.2, { right: true });
3095
+ let firstRead = -1;
3096
+ run(0.5, { torch: true }, (i) => { if (firstRead < 0 && s.band.text === L) firstRead = i + 1; });
3097
+ const firstReadMs = Math.round(firstRead * 1000 / 60);
3098
+ const theTorchReadsIt = firstRead > 0 && firstReadMs < wait && forge.seen;
3099
+
3100
+ // 3. TORCH DOWN, STILL STOOD THERE: the same letter again, for nothing.
3101
+ run(0.3);
3102
+ run(wait / 1000 + 0.2);
3103
+ const readsAgainInTheDark = s.torch.raise < 0.01 && s.band.text === L;
3104
+
3105
+ // 4. COMPARING IS ONE BUTTON. A fragment in his hand; hold Q and the band
3106
+ // reads it, let go and it reads the door. Twice, and the torch is never
3107
+ // pressed. It blanks at every switch: two pieces of paper, not a label.
3108
+ s.prince.inventory.push({ kept: 2, answers: null, seen: true });
3109
+ s.sheaf.pages.push({ kind: 'fragment', kept: 2 });
3110
+ s.sheaf.index = s.sheaf.pages.length - 1;
3111
+ const F = s.letters.byHouse[2].fragment;
3112
+ const switches = [];
3113
+ let last = s.band.text;
3114
+ const watch = () => {
3115
+ if (s.band.text && s.band.text !== last) {
3116
+ switches.push({ to: s.band.text === F ? 'page' : s.band.text === L ? 'door' : 'other',
3117
+ alpha: +s.band.alpha.toFixed(3) });
3118
+ }
3119
+ if (s.band.text) last = s.band.text;
3120
+ };
3121
+ for (let k = 0; k < 2; k++) {
3122
+ run(0.8, { sheaf: true }, watch);
3123
+ run(0.6, {}, watch);
3124
+ }
3125
+ const order = switches.map((w) => w.to).join(',');
3126
+ const oneButton = order === 'page,door,page,door';
3127
+ const blanksEachTime = switches.length > 0 && switches.every((w) => w.alpha < 0.1);
3128
+
3129
+ // 5. EVERY DOOR LIT, THEN THE WHOLE STREET WALKED WITH THE TORCH DOWN:
3130
+ // closed on every frame he is walking.
3131
+ for (const h of s.houses) { standAt(h.x - 300); run(1.4, { torch: true }); }
3132
+ const allLit = s.houses.every((h) => h.seen);
3133
+ standAt(s.houses[0].x - 700);
3134
+ const end = s.houses[s.houses.length - 1].x + 600;
3135
+ let walked = 0;
3136
+ let openWhileWalking = 0;
3137
+ while (s.prince.x < end && walked < 60 * 60) {
3138
+ step({ right: true });
3139
+ walked++;
3140
+ if (s.band.text) openWhileWalking++;
3141
+ }
3142
+ const closedWhileWalking = allLit && walked > 60 && openWhileWalking === 0;
3143
+
3144
+ // 6. STOPPING AT A LIT DOOR OPENS IT AFTER READ_STILL_MS, AND NOT BEFORE.
3145
+ standAt(forge.x - 700);
3146
+ while (s.prince.x < forge.x - 300) step({ right: true });
3147
+ let opened = -1;
3148
+ for (let i = 0; i < 60; i++) {
3149
+ step();
3150
+ if (opened < 0 && s.band.text === L) opened = i + 1;
3151
+ }
3152
+ const openedMs = Math.round(opened * 1000 / 60);
3153
+ const waitsThenOpens = opened > 0 && openedMs >= wait - 1 && openedMs <= wait + 50;
3154
+
3155
+ // 7. A SWING IS NOT STANDING STILL. Nothing flashes up mid-fight.
3156
+ let swingFrames = 0;
3157
+ let openWhileSwinging = 0;
3158
+ step({ attack: true });
3159
+ while (s.prince.attacking && swingFrames < 120) {
3160
+ swingFrames++;
3161
+ if (s.band.text) openWhileSwinging++;
3162
+ step();
3163
+ }
3164
+ const closedWhileSwinging = swingFrames > 0 && openWhileSwinging === 0;
3165
+
3166
+ // 8. OUT OF REACH: closed.
3167
+ standAt(forge.x - 900);
3168
+ run(wait / 1000 + 0.5);
3169
+ const closedOutOfReach = s.band.text == null;
3170
+
3171
+ scene.reset();
3172
+ return {
3173
+ name: 'the band reads the door',
3174
+ pass: neverLitIsClosed && theTorchReadsIt && readsAgainInTheDark &&
3175
+ oneButton && blanksEachTime && closedWhileWalking && waitsThenOpens &&
3176
+ closedWhileSwinging && closedOutOfReach && notTheDoorEActsOn === 0,
3177
+ neverLitIsClosed,
3178
+ theTorchReadsItFirst: theTorchReadsIt, firstReadAfterMs: firstReadMs,
3179
+ readsAgainWithTheTorchDown: readsAgainInTheDark,
3180
+ comparingIsOneButton: oneButton, switches: order,
3181
+ andItBlanksAtEachSwitch: blanksEachTime,
3182
+ closedWhileWalkingPastLitDoors: closedWhileWalking, framesWalked: walked,
3183
+ opensAfterStandingStillMs: openedMs, readStillMs: wait,
3184
+ waitsThenOpens, closedWhileSwinging, closedOutOfReach,
3185
+ framesReadingADoorEWouldNotActOn: notTheDoorEActsOn, framesSampled: frames,
3186
+ };
3187
+ }
3188
+
3189
+ /**
3190
+ * THE DOOR CARRIES FOUR THINGS: the letter, the slot, the key and the room.
3191
+ *
3192
+ * None may cover another β€” except the key, which has always hung INSIDE the
3193
+ * room by the jamb, because that is where a key to your own front door goes.
3194
+ * The room is measured off the art rather than written down: the lit
3195
+ * doorway's own pixels, and 16px round them for the things that poke out of
3196
+ * it, the forge's anvil horn furthest.
3197
+ *
3198
+ * AND BOTH PAPERS ARE LIT FROM WHERE HE STANDS, on every frame of three
3199
+ * seconds of flicker rather than on one: a check that passes or fails on
3200
+ * which frame it sampled is not a check. Within DOOR_REACH too, which is
3201
+ * where E and the band both measure from.
3202
+ */
3203
+ function theDoorCarriesFour() {
3204
+ const clip = s.houseClip;
3205
+ const clear = (a, b) =>
3206
+ a[0] + a[2] <= b[0] || b[0] + b[2] <= a[0] || a[1] + a[3] <= b[1] || b[1] + b[3] <= a[1];
3207
+ const roomBox = (v) => {
3208
+ const off = new OffscreenCanvas(clip.width, clip.height);
3209
+ const o = off.getContext('2d', { willReadFrequently: true });
3210
+ o.drawImage(clip.frames[v].lit, 0, 0);
3211
+ const d = o.getImageData(0, 0, clip.width, clip.height).data;
3212
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
3213
+ for (let y = 0; y < clip.height; y++) {
3214
+ for (let x = 0; x < clip.width; x++) {
3215
+ const i = (y * clip.width + x) * 4;
3216
+ if (d[i + 3] < 250 || Math.abs(d[i] - 0xd8) > 6 ||
3217
+ Math.abs(d[i + 1] - 0xc3) > 6 || Math.abs(d[i + 2] - 0xa0) > 6) continue;
3218
+ if (x < x0) x0 = x;
3219
+ if (x > x1) x1 = x;
3220
+ if (y < y0) y0 = y;
3221
+ if (y > y1) y1 = y;
3222
+ }
3223
+ }
3224
+ return [x0 - 16, y0 - 16, x1 - x0 + 33, y1 - y0 + 33];
3225
+ };
3226
+ const rows = s.houses.map((h) => {
3227
+ const L = clip.letterAt[h.variant];
3228
+ const S = clip.slotAt[h.variant];
3229
+ const K = clip.keyAt[h.variant];
3230
+ const R = roomBox(h.variant);
3231
+ scene.reset();
3232
+ s.prince.x = s.prince.prevX = h.x - 300;
3233
+ s.prince.facing = 1;
3234
+ s.camera.reset(s.prince.x);
3235
+ drive(0.6, { torch: true });
3236
+ // The margin by which each paper is inside the reveal circle, at the
3237
+ // worst frame. Positive is lit.
3238
+ let letterMargin = Infinity;
3239
+ let slotMargin = Infinity;
3240
+ const inside = (p) => REVEAL_T * s.torch.radius - Math.hypot(p[0] - s.torch.cx, p[1] - s.torch.cy);
3241
+ for (let i = 0; i < 180; i++) {
3242
+ scene.update(1 / 60, { ...NONE, torch: true });
3243
+ letterMargin = Math.min(letterMargin, inside(s.letterPoint(h)));
3244
+ slotMargin = Math.min(slotMargin, inside(s.slotPoint(h)));
3245
+ }
3246
+ const lp = s.letterPoint(h);
3247
+ const sp = s.slotPoint(h);
3248
+ return {
3249
+ house: h.kind, room: R,
3250
+ letterClearOfSlot: clear(L, S), letterClearOfKey: clear(L, K),
3251
+ slotClearOfKey: clear(S, K), letterClearOfRoom: clear(L, R),
3252
+ slotClearOfRoom: clear(S, R), keyInsideTheRoom: !clear(K, R),
3253
+ letterLitEveryFrame: letterMargin > 0, slotLitEveryFrame: slotMargin > 0,
3254
+ worstLetterMargin: Math.round(letterMargin), worstSlotMargin: Math.round(slotMargin),
3255
+ inReach: Math.abs(s.prince.x - lp[0]) <= TUNING.DOOR_REACH &&
3256
+ Math.abs(s.prince.x - sp[0]) <= TUNING.DOOR_REACH,
3257
+ };
3258
+ });
3259
+ scene.reset();
3260
+ const ok = rows.every((r) => r.letterClearOfSlot && r.letterClearOfKey &&
3261
+ r.slotClearOfKey && r.letterClearOfRoom && r.slotClearOfRoom && r.keyInsideTheRoom &&
3262
+ r.letterLitEveryFrame && r.slotLitEveryFrame && r.inReach);
3263
+ return {
3264
+ name: 'the door carries four',
3265
+ pass: ok && rows.length === 5,
3266
+ everyDoor: ok,
3267
+ rows,
3268
+ };
3269
+ }
3270
+
3271
+ /**
3272
+ * THE GATEWAY IS HIS ONCE ITS KEEPER HAS GONE HOME.
3273
+ *
3274
+ * While the gatekeeper holds his post, the shut leaf stops the prince just
3275
+ * inside the left post. Once the keeper has walked home, he can walk the
3276
+ * whole width of the leaf, under all four sockets, to just inside the right
3277
+ * post. Found in play: stood at the left edge, a player could not get close
3278
+ * enough to count what the gate wanted. The view follows him in far enough to
3279
+ * frame the whole gateway.
3280
+ *
3281
+ * AND THE LEAF STILL HAS TO LIFT, AND HE STILL HAS TO WALK. Standing under
3282
+ * it when it opens does not end the level; a step through it does.
3283
+ */
3284
+ function theGateRoomOpens() {
3285
+ // The posts' outer edges, as render.js draws the gate: a 420px half-leaf
3286
+ // and a 100px post each side.
3287
+ const EDGE = 520;
3288
+ const toGate = (secs = 6) => {
3289
+ s.prince.x = s.prince.prevX = s.gateX - 900;
3290
+ s.prince.facing = 1;
3291
+ s.camera.reset(s.prince.x);
3292
+ drive(secs, { right: true });
3293
+ };
3294
+ const keeper = gatekeeper();
3295
+
3296
+ // 1. Down, but at his post: the old stop. A fight does not move him.
3297
+ const carriers = armed();
3298
+ toGate();
3299
+ const heldShut = Math.round(s.prince.x) === s.gateStopX;
3300
+
3301
+ // 2. His scrap on his own door, and he gets up and walks home.
3302
+ atDoor(4, 4);
3303
+ tap({ sheaf: true });
3304
+ drive(3);
3305
+ const goneHome = keeper.state === 'HOMING' || keeper.state === 'HOME';
3306
+
3307
+ // 3. The whole gateway, and no further β€” with all of it in the frame.
3308
+ toGate();
3309
+ const walkedIn = Math.round(s.prince.x) === s.gateRoomX;
3310
+ const cam = s.camera.x;
3311
+ const framed = s.gateX - EDGE - cam >= 0 && s.gateX + EDGE - cam <= W &&
3312
+ s.prince.x - cam <= W - 200;
3313
+
3314
+ // 4. Walking into the far post with the leaf shut ends nothing.
3315
+ drive(1, { right: true });
3316
+ const shutEndsNothing = !s.gate.through && s.fade === 0;
3317
+
3318
+ // 5. The leaf lifts over him while he stands under it, and he is still
3319
+ // there. One step takes him through.
3320
+ s.prince.x = s.prince.prevX = s.gateX + 100;
3321
+ s.prince.keys = TUNING.GATE_WANTS;
3322
+ s.sheaf.index = 0;
3323
+ for (let i = 0; i < TUNING.GATE_WANTS; i++) tap();
3324
+ drive(1 + TUNING.GATE_OPEN_MS / 1000);
3325
+ const openOverHim = s.gate.open >= 1 && !s.gate.through;
3326
+ drive(0.3, { right: true });
3327
+ const aStepTakesHimThrough = s.gate.through;
3328
+
3329
+ // 6. A restart puts the keeper back on his post, and the gateway shuts.
3330
+ scene.reset();
3331
+ toGate();
3332
+ const shutAgain = Math.round(s.prince.x) === s.gateStopX;
3333
+
3334
+ return {
3335
+ name: 'the gate room opens',
3336
+ pass: carriers.length === TUNING.GATE_WANTS && heldShut && goneHome && walkedIn &&
3337
+ framed && shutEndsNothing && openOverHim && aStepTakesHimThrough && shutAgain,
3338
+ whileHeHoldsThePost: heldShut, keeperWalkedHome: goneHome,
3339
+ walksTheWholeGateway: walkedIn, stopsAt: Math.round(s.gateRoomX - s.gateX),
3340
+ theWholeGatewayIsInFrame: framed,
3341
+ shutEndsNothing, openOverHimAndStillThere: openOverHim, aStepTakesHimThrough,
3342
+ shutAgainAfterARestart: shutAgain,
3343
  };
3344
  }
3345
 
 
3738
  orderIsPageOne(), openingHolds(),
3739
  nobodyBlocksTheRoad(), gatekeeperIsOnTheRoad(), stepDownTelegraphs(),
3740
  watchesYouPass(), laneReads(), theGlanceStillWorks(),
3741
+ wrongDoorWakes(), gatekeeperWakesAtHisPost(), theTreeTeaches(),
3742
+ lettersStay(), theSlotIsAlwaysThere(), bandReadsTheDoor(), theDoorCarriesFour(),
3743
+ theGateRoomOpens()];
3744
  return { pass: out.every((r) => r.pass), results: out };
3745
  }
3746
 
 
3775
  nobodyBlocksTheRoad, gatekeeperIsOnTheRoad, stepDownTelegraphs,
3776
  watchesYouPass, laneReads, theGlanceStillWorks,
3777
  wrongDoorWakes, gatekeeperWakesAtHisPost, theTreeTeaches,
3778
+ lettersStay, theSlotIsAlwaysThere, bandReadsTheDoor, theDoorCarriesFour,
3779
+ theGateRoomOpens,
3780
  all, frameAt, frameAtX };
3781
 
3782
  return Object.fromEntries(Object.entries(api).map(([k, fn]) => [k, silent(fn)]));
src/input.js CHANGED
@@ -166,9 +166,10 @@ export function createInput(target, { onReset } = {}) {
166
  *
167
  * The top page of the pile is now what he is holding, so TAKE acts on it
168
  * wherever he stands and the second button is deleted. Whether this press
169
- * takes a letter, pins a page, takes one back down, takes a key or picks
170
- * something off the ground is decided by where he is standing β€” which is
171
- * what TAKE has meant everywhere else in this game since the first object.
 
172
  */
173
  const takeBtn = pad?.querySelector('.wheel [data-act="pickup"]');
174
  let takeLive = false;
 
166
  *
167
  * The top page of the pile is now what he is holding, so TAKE acts on it
168
  * wherever he stands and the second button is deleted. Whether this press
169
+ * pins a page, takes one back down, takes a key or picks something off the
170
+ * ground is decided by where he is standing β€” which is what TAKE has meant
171
+ * everywhere else in this game since the first object. Letters are not on
172
+ * that list: they stay on the door.
173
  */
174
  const takeBtn = pad?.querySelector('.wheel [data-act="pickup"]');
175
  let takeLive = false;
src/main.js CHANGED
@@ -117,7 +117,7 @@ const loop = createLoop({
117
  // EVERY OFFER THE SCENE MAKES, and the scene is the only thing that
118
  // decides. This used to re-run the ground scan itself, which was two
119
  // places that had to agree about what is within reach.
120
- input.setTakeAvailable?.(st.canMark || st.canTakeLetter || st.canTakeKey ||
121
  st.canPlaceKey || st.canTake);
122
  }
123
  },
 
117
  // EVERY OFFER THE SCENE MAKES, and the scene is the only thing that
118
  // decides. This used to re-run the ground scan itself, which was two
119
  // places that had to agree about what is within reach.
120
+ input.setTakeAvailable?.(st.canMark || st.canTakeKey ||
121
  st.canPlaceKey || st.canTake);
122
  }
123
  },
src/render.js CHANGED
@@ -266,9 +266,9 @@ export function createRenderer(ctx) {
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;
@@ -280,10 +280,10 @@ export function createRenderer(ctx) {
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);
@@ -409,7 +409,7 @@ export function createRenderer(ctx) {
409
  // only shows where the light actually falls.
410
  if (mode === 'lit') {
411
  c.drawImage(clip.frames[h.variant].lit, x, y);
412
- drawNail(c, s, clip, h, x, y, s.clock ?? 0);
413
  drawRoomKey(c, s, clip, h, x, y, s.clock ?? 0);
414
  } else {
415
  c.drawImage(s.houseArt[h.variant], x, y);
@@ -614,13 +614,19 @@ export function createRenderer(ctx) {
614
  }
615
 
616
  /**
617
- * A warm pulse at a door, OUT IN THE DARK, with the torch down.
 
618
  *
619
- * The patch and the key only ever existed inside the torchlight, because
620
- * everything in a doorway is drawn in the lit pass. So the street said
621
- * nothing: you had to already be holding the light on a door to learn there
622
- * was anything at it, which is the wrong way round β€” the light is what you
623
- * spend, and something has to be worth spending it on.
 
 
 
 
 
624
  *
625
  * THE DROPPED SCRAPS HAVE ALWAYS DONE THIS. "A warm pulse out there is a
626
  * thing you notice from across the screen, and it is the only reason to walk
@@ -675,9 +681,7 @@ export function createRenderer(ctx) {
675
  }
676
  };
677
 
678
- // An empty mark, once the letter is off it and there is somewhere to pin.
679
- if (house.letterTaken && house.filed == null) at(clip.nailAt?.[house.variant], s.kept);
680
- // And a key whose door somebody opened.
681
  if (house.open && !house.keyTaken) at(clip.keyAt?.[house.variant], s.keyArt);
682
  }
683
 
@@ -955,13 +959,11 @@ export function createRenderer(ctx) {
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;
@@ -1124,32 +1126,28 @@ export function createRenderer(ctx) {
1124
  }
1125
 
1126
  /**
1127
- * THE LETTER LYING IN THE ROOM.
1128
  *
1129
- * It is an object, and it behaves like every other object in this game: it is
1130
- * there, the torch shows it, and take picks it up. It used to fall into the
1131
- * sheaf the moment the door lit β€” so the only way to READ it was to raise the
1132
- * sheaf and page to it, and anyone who had not worked out that the sheaf
1133
- * pages never saw a word of the writing.
1134
  *
1135
  * NO GLOW. The objects on the ground pulse because a dark street gives no
1136
  * other reason to walk over to one; a door is already that reason, and a glow
1137
  * bleeding through an unlit house would break the one rule the whole look
1138
  * rests on β€” that a building is a black mass until he lights it.
1139
- *
1140
- * Gone from the nail the moment he takes it. What is left is the patch of
1141
- * cleaner wall it kept the weather off, waiting for the other half.
1142
  */
1143
  // The same value a lit doorway is drawn at, and it has to be: a page held in
1144
  // that light is the same paper as the light falling out of the room.
1145
  const DOORLIGHT = '#d8c3a0';
1146
 
1147
  /**
1148
- * THE WALL PATCH, and it is what a deleted frame left behind.
1149
  *
1150
- * A letter hung on this nail for sixty years and kept the weather off the
1151
- * wall under it. Take the letter and that rectangle of cleaner wall is what
1152
- * is left β€” and it is where a fragment goes.
1153
  *
1154
  * IT IS A VALUE, NOT AN OUTLINE. No border, no socket, no glow, and it must
1155
  * never read as UI. The palette makes that a narrow window: the wall sits at
@@ -1159,9 +1157,8 @@ export function createRenderer(ctx) {
1159
  */
1160
  const PATCH = '#6f6553'; // lum 101 β€” over the forbidden zone, under the door
1161
 
1162
- /** Where the nail's one spot lands on screen, for whatever is hanging on it. */
1163
- function nailBox(clip, house, x, y, art) {
1164
- const rect = clip.nailAt?.[house.variant];
1165
  if (!rect || !art) return null;
1166
  const [nx, ny, nw, nh] = rect;
1167
  const ink = art.ink ?? { x: 0, y: 0, w: art.width, h: art.height };
@@ -1173,78 +1170,49 @@ export function createRenderer(ctx) {
1173
  }
1174
 
1175
  /**
1176
- * WHAT IS ON THE NAIL. One spot, three states, in the order a player meets
1177
- * them: the letter somebody left, the mark it left, and the answer he pins
1178
- * back into that mark.
1179
  *
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.
1187
- if (!house.letterTaken) {
1188
- const art = s.letterArt;
1189
- const frame = art?.frames[house.index];
1190
- const b = nailBox(clip, house, x, y, art);
1191
- if (!b || !frame) return;
1192
  // PAPER FIRST. The page art is contour and writing on nothing, so on a
1193
  // dark wall it came out dark-on-dark. It is a sheet of paper in
1194
  // torchlight, so it gets the value a lit doorway gets.
1195
  c.fillStyle = DOORLIGHT;
1196
- c.fillRect(Math.round(b.px - 3), Math.round(b.py - 3),
1197
- Math.round(b.w + 6), Math.round(b.h + 6));
1198
- c.drawImage(frame.lit, b.px - b.ink.x * b.k, b.py - b.ink.y * b.k,
1199
- art.width * b.k, art.height * b.k);
1200
- return;
1201
  }
 
1202
 
1203
- // 2. THE PATCH. Flat, borderless, and slightly bigger than the page that
1204
- // made it, because paper curls and the weather got in at the edges.
1205
- const rect = clip.nailAt[house.variant];
 
 
1206
  const [nx, ny, nw, nh] = rect;
1207
  const px0 = Math.round(x + nx);
1208
  const py0 = Math.round(y + ny);
1209
 
1210
- // 2a. AND WHILE IT IS EMPTY IT BREATHES.
 
 
 
 
 
1211
  //
1212
- // The ghost below only helps a player who is ALREADY standing here with
1213
- // the right thing in his hand. Nothing walked him over in the first place:
1214
- // he lit a door, saw a slightly cleaner rectangle of wall, and had no
1215
- // reason to think it was anything. That is what actually happened to the
1216
- // first person who played it.
1217
- //
1218
- // THE GAME ALREADY HAS A WORD FOR THIS and it is not a new one. A scrap
1219
- // lying in the dark breathes; so does a key once its door is open. Same
1220
- // numbers, same rhythm, same meaning β€” something here is yours to do
1221
- // something with. A new kind of highlight would have been UI; this is the
1222
- // sentence the level has been speaking since the first object on the
1223
- // ground.
1224
- //
1225
- // IT STOPS THE MOMENT SOMETHING IS IN IT. An empty place asks its own
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;
1236
- const cy = py0 + nh / 2;
1237
- const r = Math.max(nw, nh) * TORCH.KEPT_GLOW_R;
1238
- const g = c.createRadialGradient(cx, cy, 0, cx, cy, r);
1239
- g.addColorStop(0, `rgba(${TORCH.KEPT_RGB},${(TORCH.KEPT_GLOW * pulse).toFixed(3)})`);
1240
- g.addColorStop(1, `rgba(${TORCH.KEPT_RGB},0)`);
1241
- const was = c.globalCompositeOperation;
1242
- c.globalCompositeOperation = 'lighter';
1243
- c.fillStyle = g;
1244
- c.fillRect(cx - r, cy - r, r * 2, r * 2);
1245
- c.globalCompositeOperation = was;
1246
- }
1247
-
1248
  // IT IS THE SHAPE OF WHAT GOES IN IT.
1249
  //
1250
  // A rectangle says "a place" without saying what for, and that is exactly
@@ -1291,11 +1259,11 @@ export function createRenderer(ctx) {
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];
1298
- const gb = nailBox(clip, house, x, y, ghostArt);
1299
  if (!gf || !gb) return;
1300
  c.globalAlpha = 0.5;
1301
  c.drawImage(gf.lit, gb.px - gb.ink.x * gb.k, gb.py - gb.ink.y * gb.k,
@@ -1313,7 +1281,7 @@ export function createRenderer(ctx) {
1313
  // up the street, and that takes as long as it takes.
1314
  const art = s.kept;
1315
  const frame = art?.frames[house.filed];
1316
- const b = nailBox(clip, house, x, y, art);
1317
  if (!b || !frame) return;
1318
  c.drawImage(frame.lit, b.px - b.ink.x * b.k, b.py - b.ink.y * b.k,
1319
  art.width * b.k, art.height * b.k);
@@ -1836,10 +1804,9 @@ export function createRenderer(ctx) {
1836
  : 'β€”') +
1837
  (s.canPin ? ' [E] PIN IT HERE'
1838
  : s.canMark ? ' [E] take it back down'
1839
- : s.canTakeLetter ? ' [E] take the letter'
1840
- : s.canPlaceKey ? ` [E] put a key in (${s.prince.keys} in hand)`
1841
- : s.canTakeKey ? ' [E] take the key'
1842
- : nearKept ? ' [E] take' : '') +
1843
  // ONLY WHEN IT IS NOT WORKING. Spelled out because a phone has no
1844
  // console and no backtick key β€” "running / playback" and still silent
1845
  // on an iPhone means the ringer switch, not the game. Once it is
 
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 note on its
270
+ * nail, drawn by drawNail like every letter. It stays there, and there is
271
+ * no slot under it: nothing is ever pinned back to a tree.
272
  */
273
  function drawTree(c, s, camX, mode) {
274
  const t = s.tree;
 
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, { slot: false });
284
+ } else if (s.treeNoteDark) {
285
  const art = s.letterArt;
286
+ const b = nailBox(clip.letterAt?.[t.variant], 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);
 
409
  // only shows where the light actually falls.
410
  if (mode === 'lit') {
411
  c.drawImage(clip.frames[h.variant].lit, x, y);
412
+ drawNail(c, s, clip, h, x, y);
413
  drawRoomKey(c, s, clip, h, x, y, s.clock ?? 0);
414
  } else {
415
  c.drawImage(s.houseArt[h.variant], x, y);
 
614
  }
615
 
616
  /**
617
+ * A warm pulse at a door, OUT IN THE DARK, with the torch down β€” for a key
618
+ * whose door somebody opened, and nothing else.
619
  *
620
+ * The key only ever existed inside the torchlight, because everything in a
621
+ * doorway is drawn in the lit pass. So the street said nothing: you had to
622
+ * already be holding the light on a door to learn there was anything at it,
623
+ * which is the wrong way round β€” the light is what you spend, and something
624
+ * has to be worth spending it on.
625
+ *
626
+ * THE EMPTY SLOT USED TO PULSE HERE TOO, and does not any more. It is a
627
+ * value, not a glow: a mark on a wall that breathes in the dark is UI, and
628
+ * the loom house's slot, which nothing ever fills, would have been asking
629
+ * for the whole level in a voice the game uses for "take this".
630
  *
631
  * THE DROPPED SCRAPS HAVE ALWAYS DONE THIS. "A warm pulse out there is a
632
  * thing you notice from across the screen, and it is the only reason to walk
 
681
  }
682
  };
683
 
684
+ // A key whose door somebody opened.
 
 
685
  if (house.open && !house.keyTaken) at(clip.keyAt?.[house.variant], s.keyArt);
686
  }
687
 
 
959
  function drawLetterHouse(c, s, sheet, x0, y0, pg) {
960
  if (!pg || pg.kind !== 'letter') return;
961
  const house = s.houses?.[pg.house];
962
+ // WHERE IT CAME FROM. The only letter he carries is his orders, and they
963
+ // came from the palace. House letters and the note on the tree stay where
964
+ // they were left, so they are never a page here β€” but a letter with a
965
+ // house behind it would still draw that house.
966
+ const hc = house ? s.houseClip : s.palaceClip;
 
 
967
  if (!hc) return;
968
  const frame = house ? house.variant : 0;
969
  const bx = x0 + sheet.width / 2;
 
1126
  }
1127
 
1128
  /**
1129
+ * THE LETTER ON THE DOOR, and it never comes down.
1130
  *
1131
+ * Letters stay where they were left; fragments come off bodies. He does not
1132
+ * pocket a stranger's mail, and that puts the deduction where it belongs:
1133
+ * the question is nailed in front of him, the fragment is in his hand, and
1134
+ * the door is where he judges.
 
1135
  *
1136
  * NO GLOW. The objects on the ground pulse because a dark street gives no
1137
  * other reason to walk over to one; a door is already that reason, and a glow
1138
  * bleeding through an unlit house would break the one rule the whole look
1139
  * rests on β€” that a building is a black mass until he lights it.
 
 
 
1140
  */
1141
  // The same value a lit doorway is drawn at, and it has to be: a page held in
1142
  // that light is the same paper as the light falling out of the room.
1143
  const DOORLIGHT = '#d8c3a0';
1144
 
1145
  /**
1146
+ * THE SLOT, and it is what a deleted frame left behind.
1147
  *
1148
+ * Their own paper hung on a second nail under the letter left for them, and
1149
+ * kept the weather off the wall behind it. The fragment on a body is that
1150
+ * paper, and the lighter patch is where it goes back.
1151
  *
1152
  * IT IS A VALUE, NOT AN OUTLINE. No border, no socket, no glow, and it must
1153
  * never read as UI. The palette makes that a narrow window: the wall sits at
 
1157
  */
1158
  const PATCH = '#6f6553'; // lum 101 β€” over the forbidden zone, under the door
1159
 
1160
+ /** Where a rect on the wall lands on screen, for whatever is hanging in it. */
1161
+ function nailBox(rect, x, y, art) {
 
1162
  if (!rect || !art) return null;
1163
  const [nx, ny, nw, nh] = rect;
1164
  const ink = art.ink ?? { x: 0, y: 0, w: art.width, h: art.height };
 
1170
  }
1171
 
1172
  /**
1173
+ * WHAT IS ON THE WALL BESIDE A DOOR, top to bottom: the letter somebody
1174
+ * left, and under it the slot β€” the patch, the bare nail, and whatever he
1175
+ * pins there. The question above the answer, and the eye reads down.
1176
  *
1177
+ * LIT PASS ONLY, so all of it lives and dies with the torch like everything
1178
  * else in a doorway.
1179
+ *
1180
+ * THE TREE HAS NO SLOT (`slot: false`). It carries the note and nothing
1181
+ * else: nothing is ever pinned back to a tree.
1182
  */
1183
+ function drawNail(c, s, clip, house, x, y, { slot = true } = {}) {
1184
+ // 1. THE LETTER. Always: nothing takes it down.
1185
+ const paper = s.letterArt;
1186
+ const written = paper?.frames[house.index];
1187
+ const lb = nailBox(clip.letterAt?.[house.variant], x, y, paper);
1188
+ if (lb && written) {
 
 
 
1189
  // PAPER FIRST. The page art is contour and writing on nothing, so on a
1190
  // dark wall it came out dark-on-dark. It is a sheet of paper in
1191
  // torchlight, so it gets the value a lit doorway gets.
1192
  c.fillStyle = DOORLIGHT;
1193
+ c.fillRect(Math.round(lb.px - 3), Math.round(lb.py - 3),
1194
+ Math.round(lb.w + 6), Math.round(lb.h + 6));
1195
+ c.drawImage(written.lit, lb.px - lb.ink.x * lb.k, lb.py - lb.ink.y * lb.k,
1196
+ paper.width * lb.k, paper.height * lb.k);
 
1197
  }
1198
+ if (!slot) return;
1199
 
1200
+ // 2. THE SLOT, UNDER IT. Flat, borderless, and slightly bigger than the
1201
+ // page that made it, because paper curls and the weather got in at the
1202
+ // edges. There from the first light, whether or not anything is in it.
1203
+ const rect = clip.slotAt?.[house.variant];
1204
+ if (!rect) return;
1205
  const [nx, ny, nw, nh] = rect;
1206
  const px0 = Math.round(x + nx);
1207
  const py0 = Math.round(y + ny);
1208
 
1209
+ // IT DOES NOT BREATHE. It used to, while it was empty: the patch only
1210
+ // appeared once the letter came off, and nothing walked a player over to a
1211
+ // door to find it. It is under the letter from the first light now, and a
1212
+ // bare nail under a letter asks its own question. A pulse on a wall reads
1213
+ // as UI, and the loom house's empty slot has to read as a letter nobody
1214
+ // answered, not as a button nobody pressed.
1215
  //
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1216
  // IT IS THE SHAPE OF WHAT GOES IN IT.
1217
  //
1218
  // A rectangle says "a place" without saying what for, and that is exactly
 
1259
  // AND IT IS NOT A VERDICT. It appears for the wrong page just as readily,
1260
  // because it is a preview of an act and not an opinion about it.
1261
  if (house.filed == null) {
1262
+ if (!s.canPin || s.markHouse !== house.index) return;
1263
  const held = s.handPage;
1264
  const ghostArt = held?.kind === 'fragment' ? s.kept : null;
1265
  const gf = ghostArt?.frames[held.kept];
1266
+ const gb = nailBox(rect, x, y, ghostArt);
1267
  if (!gf || !gb) return;
1268
  c.globalAlpha = 0.5;
1269
  c.drawImage(gf.lit, gb.px - gb.ink.x * gb.k, gb.py - gb.ink.y * gb.k,
 
1281
  // up the street, and that takes as long as it takes.
1282
  const art = s.kept;
1283
  const frame = art?.frames[house.filed];
1284
+ const b = nailBox(rect, x, y, art);
1285
  if (!b || !frame) return;
1286
  c.drawImage(frame.lit, b.px - b.ink.x * b.k, b.py - b.ink.y * b.k,
1287
  art.width * b.k, art.height * b.k);
 
1804
  : 'β€”') +
1805
  (s.canPin ? ' [E] PIN IT HERE'
1806
  : s.canMark ? ' [E] take it back down'
1807
+ : s.canPlaceKey ? ` [E] put a key in (${s.prince.keys} in hand)`
1808
+ : s.canTakeKey ? ' [E] take the key'
1809
+ : nearKept ? ' [E] take' : '') +
 
1810
  // ONLY WHEN IT IS NOT WORKING. Spelled out because a phone has no
1811
  // console and no backtick key β€” "running / playback" and still silent
1812
  // on an iPhone means the ringer switch, not the game. Once it is
src/scene_level.js CHANGED
@@ -201,12 +201,23 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
201
  // thing that says the mode changed.
202
  const GATE_X = BOWL.X1;
203
  const GATE_STOP_X = GATE_X - 380; // inside the gateway, clear of the post
 
 
 
 
204
  // Where the view stops, and it is not simply the gate. Bounding the camera at
205
  // GATE_X - W/2 put the wall across the middle of the frame and shoved him
206
  // into the left half to stand at it. Stopping three quarters of the way over
207
  // leaves the village behind him visible while he is working out that the gate
208
  // will not open, which is the exact moment he has to turn around.
209
  const GATE_CAM_X = GATE_X - W * 0.75;
 
 
 
 
 
 
 
210
 
211
  // THERE IS NO ONE-WAY MOUTH, and the reason is worth keeping.
212
  //
@@ -291,21 +302,20 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
291
  variant: i,
292
  x: houseX(i),
293
  w: houseClip ? houseClip.width : 960,
294
- seen: false, // has the torch been held on its door? Latches, like the
295
- // objects on the ground, and for the same reason
 
 
 
 
 
 
 
296
  // WHAT HE HAS FILED UNDER THIS HOUSE, and nothing else in the build writes
297
  // it. The `kept` index of one fragment, or null. The map used to fill this
298
  // in by itself the moment he looked at something, which made it a receipt;
299
  // a claim only means anything if it can be wrong, so he makes it.
300
  filed: null,
301
- // THE LETTER IS AN OBJECT IN THE ROOM, not a thing the house hands over for
302
- // being looked at. Light it to read it, press take to keep it β€” the same
303
- // two acts, in the same order, that every fragment on the ground already
304
- // asks for. It used to drop into the sheaf the instant the door lit, which
305
- // meant the only way to READ it was to raise the sheaf and page to it, and
306
- // a player who had not found paging never saw a word of the writing.
307
- letterSeen: false,
308
- letterTaken: false,
309
  // SOMEBODY CAME HOME TO IT. Set when a body finishes its walk at this
310
  // door, and it is the only thing in the level that is ever a verdict β€”
311
  // except that it is not one, because the game does not say it. A person
@@ -339,16 +349,26 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
339
  const doorPoint = (h) => [h.x + doorOffset[0], GROUND_Y + doorOffset[1]];
340
 
341
  /**
342
- * The middle of the nail beside that door, as a world point β€” which is the
343
- * letter while it is hanging there, and the patch it leaves, and the page
344
- * pinned into the patch. One spot, so one point, and the torch test is the
345
- * same for all three.
346
  *
347
  * Straight out of the clip's own pixels, the same deal `doorAt` gets: move it
348
  * in the art and the thing the torch has to cover moves with it.
349
  */
350
  const letterPoint = (h) => {
351
- const r = houseClip?.nailAt?.[h.variant];
 
 
 
 
 
 
 
 
 
 
 
 
352
  if (!r) return null;
353
  return [h.x - houseClip.pivotX + r[0] + r[2] / 2,
354
  GROUND_Y - houseClip.pivotY + r[1] + r[3] / 2];
@@ -394,18 +414,18 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
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];
@@ -563,6 +583,10 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
563
  { standing: true, stayDown: true, post: true, kept: HOUSE_OF.indexOf('the gatehouse'),
564
  keptMaxX: KEPT_MAX_X }));
565
  }
 
 
 
 
566
 
567
  /**
568
  * THE DEAL, AND IT IS A CONSTRAINED SHUFFLE.
@@ -703,15 +727,21 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
703
  * THE SHEAF. Loose pages he carries, and the map is one of them.
704
  *
705
  * Built here at L2 and paged at L3. What arrives when:
706
- * the map at the start
707
- * a house letter when he lights that door
708
  * a fragment when he takes it
 
 
 
 
709
  *
710
  * FOUND ORDER. Not grouped, not sorted, no index and no tabs β€” the order he
711
  * picked them up in is the only order there is.
712
  */
713
  // Last frame's take button, so an act fires on the edge. See where it is read.
714
  let prevPickup = false;
 
 
 
715
  // Whether he wanted the light last frame, so the flame fires on the raise
716
  // and not on every frame of holding it.
717
  let litLast = false;
@@ -793,9 +823,10 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
793
  const page = () => sheaf.pages[sheaf.index] ?? null;
794
 
795
  /**
796
- * Has he looked at this page's contents? A letter is read off a lit door, so
797
- * it arrives known. A FRAGMENT CAN BE TAKEN BLIND and arrives unknown, and
798
- * the whole gate at the end of the level turns on that difference.
 
799
  */
800
  const pageSeen = (pg) => {
801
  // A letter was read off a lit door and a key has nothing written on it, so
@@ -824,10 +855,10 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
824
  * Put a page on the pile and open on it.
825
  *
826
  * FOUND ORDER means it goes on the end. Moving the index to it is what a
827
- * person does with a handful of letters β€” the new one is the one in your
828
- * hand β€” and it is what makes lighting a door one gesture instead of two:
829
- * `Q` raises the torch, the door lights, the letter arrives, and it is the
830
- * page already open.
831
  */
832
  const filePage = (pg) => {
833
  sheaf.pages.push(pg);
@@ -842,8 +873,8 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
842
  * explaining that he begins carrying nothing. That was true of a pile of
843
  * things he had FOUND. This is the one page he did not find: it is what he
844
  * was handed at the palace and what sent him down this road, and by the end
845
- * of the village it sits under nine letters and fragments that between them
846
- * contradict every word of it. Nothing has to say so.
847
  *
848
  * IT IS AN ORDINARY LETTER PAGE. Not its own kind: it is a page of writing
849
  * with a building behind it, which is what every letter in this game is, and
@@ -863,10 +894,12 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
863
 
864
  /**
865
  * What the band is showing, and it shows the document rather than describing
866
- * it. At L2 that is the letter of whatever door the torch is on; at L3 the
867
- * sheaf takes it over and the band opens and closes with the pages.
 
 
868
  */
869
- const band = { text: null, alpha: 0, turns: 0 };
870
 
871
  /**
872
  * WHAT THIS BUILD IS FOR, counted.
@@ -903,7 +936,6 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
903
  sheetClip, mapClip, hurtClip, run, sheaf, band, textFor, letters, page, pageSeen, filedUnder,
904
  canMark: false,
905
  markHouse: null,
906
- canTakeLetter: false,
907
  canTakeKey: false,
908
  canPlaceKey: false,
909
  // Whether there is anything at his feet to pick up. Computed once here
@@ -911,9 +943,15 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
911
  canTake: false,
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 },
919
  torchArm: TORCH_ARM,
@@ -942,10 +980,12 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
942
  state.clock = 0;
943
  for (const h of HOUSES) {
944
  h.seen = false; h.filed = null;
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;
@@ -970,7 +1010,7 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
970
  sheaf.turns = 0;
971
  band.text = null;
972
  band.alpha = 0;
973
- band.turns = 0;
974
  state.fade = 0;
975
  // Deaths survive a restart. Everything else about the run does not β€” that
976
  // is what "the findings are at stake" means.
@@ -993,6 +1033,10 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
993
 
994
  function update(dt, input) {
995
  prince.prevX = prince.x;
 
 
 
 
996
  torch.snapshot();
997
  camera.snapshot();
998
  state.clock += dt;
@@ -1105,9 +1149,18 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
1105
  if (prince.x < 0) prince.x = 0;
1106
  // He does not hit a boundary, he hits a shut gate. Once it is up, the
1107
  // only thing left in his way is the man standing in it.
1108
- const stop = gate.open >= 1 ? GATE_X + 260 : GATE_STOP_X;
 
 
 
 
 
 
 
 
1109
  if (prince.x > stop) prince.x = stop;
1110
  }
 
1111
  camera.follow(prince.x);
1112
 
1113
  // --- The sword --------------------------------------------------------
@@ -1332,46 +1385,45 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
1332
  // Lighting one is the only thing that says who lived here. It latches, the
1333
  // same way seeing an object on the ground latches, and for the same reason:
1334
  // he looked, and looking is not something he can un-do.
1335
- // IT LATCHES AND NOTHING READS IT THIS FRAME ANY MORE. There used to be a
1336
- // `lit` house here, meaning "the torch is on this door right now", and it
1337
- // was what marking tested. Marking now tests a LATCH plus a distance, the
1338
- // same deal the letter and the key already had β€” so all this loop does is
1339
- // remember that he looked.
1340
- let litLetter = null;
1341
  for (const h of HOUSES) {
1342
  const [dx, dy] = doorPoint(h);
1343
- if (!torch.covers(dx, dy)) continue;
1344
- h.seen = true;
1345
- // And the letter on its wall. Same latch as an object on the ground: he
1346
- // looked, and looking is not something he can un-do.
1347
- if (!h.letterTaken && letterPoint(h) && torch.covers(...letterPoint(h))) {
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
1359
- // pick up in the dark once it has fallen.
 
 
 
1360
  //
1361
- // IT USED TO, AND ON A PHONE THAT MADE IT IMPOSSIBLE. TORCH and TAKE are
1362
- // both on the left-hand wheel and one thumb cannot hold one while pressing
1363
- // the other β€” so letting go of TORCH to reach TAKE un-offered the letter
1364
- // every time. On a keyboard you could hold both and never notice.
1365
  for (const h of HOUSES) {
1366
- if (h.letterTaken || !h.letterSeen) continue;
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.
@@ -1392,39 +1444,64 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
1392
 
1393
  // --- the band -----------------------------------------------------------
1394
  //
1395
- // IT OPENS WITH THE SHEAF AND CLOSES WITH IT. At L2 it hung off whatever
1396
- // door the torch was on, because there was nothing to page. Now it shows
1397
- // the page he is holding up and nothing else, which is why `Q` is the whole
1398
- // reading gesture: it raises the torch, the door in front of him lights,
1399
- // the room shows, and the letter that just arrived is the page already
1400
- // open. One button, both halves.
 
 
1401
  //
1402
- // Raising the plain torch still lights a door and still shows the room. It
1403
- // just does not read the paper out, because he is not holding any paper up.
 
1404
  //
1405
- // Verbatim, always. Nothing else in this game gets text: not doors, not the
1406
- // gate, not objects and not the prince.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1407
  // Gated on the raise so the band and the page arrive together β€” the sheet
1408
  // itself is not drawn until the arm is up, and a band that beat it there
1409
  // would read as a subtitle rather than as the paper in his hand.
 
 
 
 
1410
  const pg = sheaf.up && torch.raise > 0.9 ? page() : null;
1411
  // AN UNKNOWN PAGE HAS NOTHING ON IT YET. He took it blind and he has not
1412
  // looked; the band would be reading him words he cannot have.
1413
- //
1414
- // AND A LETTER STILL LYING IN ITS ROOM READS UNDER THE PLAIN TORCH. That is
1415
- // what "look at it" means everywhere else in this game, and making the
1416
- // writing cost a second gesture hid it from anyone who had not worked out
1417
- // that the sheaf pages. The sheaf still wins when it is up: he is holding a
1418
- // page, and the page in his hand is the one he is reading.
1419
- const want = pg && pageSeen(pg) ? textFor(pg)
1420
- : litLetter ? textFor({ kind: 'letter', house: litLetter.index })
1421
  : null;
1422
 
1423
- // CLEARED AND REFILLED ON EVERY TURN, never swapped in place. The blank
1424
- // between two pages is a fraction of a second and it is the entire reason a
1425
- // turn reads as a turn β€” swap the text and two letters in a row look like
1426
- // one letter changing its mind.
1427
- if (sheaf.turns !== band.turns) { band.turns = sheaf.turns; band.alpha = 0; }
 
 
 
 
 
 
 
1428
  band.text = want;
1429
  band.alpha = Math.min(1, Math.max(0, band.alpha + (want ? dt * 5 : -dt * 9)));
1430
 
@@ -1434,14 +1511,14 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
1434
  // more: what goes under a plot is a claim he makes, and a claim only means
1435
  // something if it can be wrong.
1436
  //
1437
- // THE ACT IS THE SAME POSTURE AS EVERYTHING ELSE. He stands at a door,
1438
- // torch up, holding a piece of paper against it, and presses take. No menu,
1439
- // no drag, no assignment screen β€” and no new button, because TAKE is
1440
- // already the contextual one and both acts are "put this where it goes".
1441
  //
1442
- // The door has to be LIT, which is the same rule reading it uses, and
1443
- // holding the sheaf raises the torch β€” so at a door the door is lit by
1444
- // definition and there is no second condition to discover.
1445
  // ONE ACT PER PRESS, DECIDED HERE AND NOT UPSTREAM.
1446
  //
1447
  // input.js already makes `pickup` a one-shot, and that was enough while
@@ -1476,42 +1553,15 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
1476
  const handPage = page();
1477
  state.handPage = handPage;
1478
 
1479
- // THE DOOR HE IS AT, AND IT IS LATCHED RATHER THAN LIT.
1480
- //
1481
- // `lit` above is whether the torch is ON a door this frame, and that was
1482
- // the marking test β€” which worked only because holding the sheaf forced the
1483
- // torch up. With the sheaf out of it, the torch is down and `lit` is null,
1484
- // so the old test could never fire again.
1485
- //
1486
- // The letter and the key already solved this: seeing latches, and taking is
1487
- // a separate act that only needs him to still be standing there. The patch
1488
- // is the third thing on that wall and it gets the same deal.
1489
- let nearDoor = null;
1490
- for (const h of HOUSES) {
1491
- if (!h.seen) continue;
1492
- const p = letterPoint(h);
1493
- if (p && Math.abs(prince.x - p[0]) <= TUNING.DOOR_REACH) { nearDoor = h; break; }
1494
- }
1495
- // AND NOT BEFORE THE LETTER IS OFF THE NAIL.
1496
- //
1497
- // There is one spot beside each door and the letter is on it. Take the
1498
- // letter and what is left is the patch of cleaner wall it kept the weather
1499
- // off for sixty years, and THAT is where a fragment goes. So a house he
1500
- // has not read has nowhere to put anything, and taking the letter is the
1501
- // thing that shows him where β€” the affordance is earned rather than drawn,
1502
- // which is the whole reason the frame went.
1503
- //
1504
- // It also closes a hole the frame never had: filing into a nail that still
1505
- // had a letter on it put a page somewhere nothing draws, so the claim was
1506
- // made and invisible.
1507
  //
1508
  // AND NOT AT A DOOR THAT IS FINISHED WITH. Someone walked home to it; the
1509
- // patch has the only thing it was ever going to hold.
1510
- //
1511
- // AND NOT AT A DOOR THAT IS FINISHED WITH. Someone walked home to it; the
1512
- // patch has the only thing it was ever going to hold.
1513
- const patchTakes = !!nearDoor && !gate.through && !nearDoor.settled &&
1514
- nearDoor.letterTaken;
1515
  const holding = handPage?.kind === 'fragment' ? handPage.kept : null;
1516
 
1517
  // PIN, or TAKE BACK, and which one is decided by what is already there.
@@ -1527,17 +1577,21 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
1527
  const canPin = patchTakes && holding !== null && holding !== nearDoor.filed;
1528
  const canMark = canPin || canRetrieve;
1529
 
1530
- // LETTER FIRST, THEN THE PATCH, THEN THE KEY. An unread door hands over the
1531
- // reading before it will accept a claim about it, and the thing that opens
1532
- // a gate comes last. Evidence before conclusions before rewards, which is
1533
- // the order this whole game is arguing for.
1534
- state.canTakeLetter = !!litLetter;
1535
- state.canMark = !litLetter && canMark;
1536
- state.canPin = !litLetter && canPin;
 
 
 
 
1537
  // WHICH house it would go under. The sheet draws that house's plot beside
1538
  // the page when this is set, so the paper itself says "this one, here".
1539
  state.markHouse = state.canPin ? nearDoor.index : null;
1540
- state.canTakeKey = !litLetter && !canMark && !!litKey;
1541
  // AND PUTTING ONE IN. He stops 380px short of the leaf and can never get
1542
  // closer, so the reach is the door's, not the ground's.
1543
  state.canPlaceKey = !canMark && prince.keys > 0 && !gate.through &&
@@ -1622,16 +1676,7 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
1622
  // second time to decide whether to light the button, which is two places
1623
  // that have to agree about what is in reach.
1624
  state.canTake = !!nearest;
1625
- // TAKING THE LETTER OUT OF THE ROOM. Third job for the same button, and it
1626
- // cannot collide with the other two: marking needs the sheaf up on a
1627
- // fragment, and this needs a letter still lying in a lit room. Ahead of the
1628
- // object at his feet because he is looking into the room, and because the
1629
- // clear pocket round every door keeps drops out of this spot anyway.
1630
- if (wantsPickup && state.canTakeLetter && !prince.attacking) {
1631
- litLetter.letterTaken = true;
1632
- filePage({ kind: 'letter', house: litLetter.index });
1633
- audio?.mark?.();
1634
- } else if (wantsPickup && state.canPlaceKey && !prince.attacking) {
1635
  // THE SAME POSTURE AS EVERYTHING ELSE. He stands at the thing and
1636
  // presses take. Four times, one socket each, and the leaf lifts on the
1637
  // fourth β€” no ceremony, no confirmation, and nothing said about the
@@ -1728,7 +1773,10 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
1728
  if (gate.filled >= TUNING.GATE_WANTS && gate.open < 1) {
1729
  gate.open = Math.min(1, gate.open + (dt * 1000) / TUNING.GATE_OPEN_MS);
1730
  }
1731
- if (gate.open >= 1 && prince.x >= GATE_X - 40) {
 
 
 
1732
  // Through it, and the playthrough is over. No prompt, no score screen,
1733
  // and no mention of the empty slot. The gate wanted four. It got four.
1734
  gate.through = true;
 
201
  // thing that says the mode changed.
202
  const GATE_X = BOWL.X1;
203
  const GATE_STOP_X = GATE_X - 380; // inside the gateway, clear of the post
204
+ // AND THE FAR SIDE OF THE SAME GATEWAY, once the man who holds it has gone
205
+ // home. 40px inside the right post, as GATE_STOP_X is inside the left, so he
206
+ // never stands in front of pure black and disappears into it.
207
+ const GATE_ROOM_X = GATE_X + 380;
208
  // Where the view stops, and it is not simply the gate. Bounding the camera at
209
  // GATE_X - W/2 put the wall across the middle of the frame and shoved him
210
  // into the left half to stand at it. Stopping three quarters of the way over
211
  // leaves the village behind him visible while he is working out that the gate
212
  // will not open, which is the exact moment he has to turn around.
213
  const GATE_CAM_X = GATE_X - W * 0.75;
214
+ // AND A LITTLE FURTHER ONCE THE GATEWAY IS HIS. With its keeper home he can
215
+ // walk to the far post, and at GATE_CAM_X that stood him 100px from the edge
216
+ // of the frame with the post cut in half. This frames the whole gateway β€”
217
+ // both posts, the leaf and all four sockets β€” with 100px of wall past it.
218
+ // The keeper leaves while the prince is at the gatehouse, a screen away, so
219
+ // the view is never at either bound when it changes and nothing jumps.
220
+ const GATE_ROOM_CAM_X = GATE_X + 620 - W;
221
 
222
  // THERE IS NO ONE-WAY MOUTH, and the reason is worth keeping.
223
  //
 
302
  variant: i,
303
  x: houseX(i),
304
  w: houseClip ? houseClip.width : 960,
305
+ // HAS THE TORCH BEEN ON ITS DOOR OR ITS LETTER? Latches, like the objects
306
+ // on the ground, and for the same reason. It is the whole of what the door
307
+ // remembers about him: the band reads a lit door again for free, and the
308
+ // slot takes a pin.
309
+ //
310
+ // THE LETTER NEVER COMES DOWN, so there is no second flag for it. Letters
311
+ // stay where they were left and fragments come off bodies: he does not
312
+ // pocket a stranger's mail, and the door is where he judges.
313
+ seen: false,
314
  // WHAT HE HAS FILED UNDER THIS HOUSE, and nothing else in the build writes
315
  // it. The `kept` index of one fragment, or null. The map used to fill this
316
  // in by itself the moment he looked at something, which made it a receipt;
317
  // a claim only means anything if it can be wrong, so he makes it.
318
  filed: null,
 
 
 
 
 
 
 
 
319
  // SOMEBODY CAME HOME TO IT. Set when a body finishes its walk at this
320
  // door, and it is the only thing in the level that is ever a verdict β€”
321
  // except that it is not one, because the game does not say it. A person
 
349
  const doorPoint = (h) => [h.x + doorOffset[0], GROUND_Y + doorOffset[1]];
350
 
351
  /**
352
+ * The middle of the letter beside that door, as a world point. It never
353
+ * comes down, so this is always where it hangs.
 
 
354
  *
355
  * Straight out of the clip's own pixels, the same deal `doorAt` gets: move it
356
  * in the art and the thing the torch has to cover moves with it.
357
  */
358
  const letterPoint = (h) => {
359
+ const r = houseClip?.letterAt?.[h.variant];
360
+ if (!r) return null;
361
+ return [h.x - houseClip.pivotX + r[0] + r[2] / 2,
362
+ GROUND_Y - houseClip.pivotY + r[1] + r[3] / 2];
363
+ };
364
+
365
+ /**
366
+ * And the middle of the slot under it: the second nail, the lighter patch
367
+ * where their own paper hung, and the page he pins back into it. Same column
368
+ * as the letter, so the reach to either is the same reach.
369
+ */
370
+ const slotPoint = (h) => {
371
+ const r = houseClip?.slotAt?.[h.variant];
372
  if (!r) return null;
373
  return [h.x - houseClip.pivotX + r[0] + r[2] / 2,
374
  GROUND_Y - houseClip.pivotY + r[1] + r[3] / 2];
 
414
  // --- the tree on the road ------------------------------------------------
415
  // A rebel nailed the rule to it: they cannot speak, what they carried is all
416
  // that is left of them, take it home. Never an answer β€” not which ones, not
417
+ // which home. It reads like a house's letter in every way the code cares
418
+ // about: light it and the band reads it, and after that standing by it
419
+ // does. It stays on the tree, the same as every letter stays on its door,
420
+ // and it is not a door β€” no slot, and nothing is ever pinned back to it.
421
  const treeClip = clips.road_tree ?? null;
422
  const TREE_NOTE = letters?.byHouse?.findIndex((h) => h.origin === 'tree') ?? -1;
423
  const TREE = treeClip && TREE_NOTE >= 0
424
+ ? { kind: 'tree', x: TREE_X, index: TREE_NOTE, variant: 0, seen: false }
 
425
  : null;
426
+ /** The middle of the note on the tree, as a world point. Off the clip's pixels. */
427
  const treePoint = () => {
428
+ const r = treeClip?.letterAt?.[0];
429
  if (!TREE || !r) return null;
430
  return [TREE.x - treeClip.pivotX + r[0] + r[2] / 2,
431
  GROUND_Y - treeClip.pivotY + r[1] + r[3] / 2];
 
583
  { standing: true, stayDown: true, post: true, kept: HOUSE_OF.indexOf('the gatehouse'),
584
  keptMaxX: KEPT_MAX_X }));
585
  }
586
+ // Whether his post is empty: walking home, or home. Nothing else takes him
587
+ // off it β€” a fight leaves him on the ground where he stood.
588
+ const GATEKEEPER = village[village.length - 1];
589
+ const keeperGone = () => GATEKEEPER.state === HOMING || GATEKEEPER.state === HOME;
590
 
591
  /**
592
  * THE DEAL, AND IT IS A CONSTRAINED SHUFFLE.
 
727
  * THE SHEAF. Loose pages he carries, and the map is one of them.
728
  *
729
  * Built here at L2 and paged at L3. What arrives when:
730
+ * his orders at the start, and permanently page one
 
731
  * a fragment when he takes it
732
+ * a key when he takes it off its hook
733
+ *
734
+ * NO HOUSE LETTERS AND NO NOTE. Those stay where they were left β€” on the
735
+ * door and on the tree β€” and the band reads them there.
736
  *
737
  * FOUND ORDER. Not grouped, not sorted, no index and no tabs β€” the order he
738
  * picked them up in is the only order there is.
739
  */
740
  // Last frame's take button, so an act fires on the edge. See where it is read.
741
  let prevPickup = false;
742
+ // How long he has stood still, in ms. The band reads a door he has lit again
743
+ // once this passes READ_STILL_MS. See the band.
744
+ let still = 0;
745
  // Whether he wanted the light last frame, so the flame fires on the raise
746
  // and not on every frame of holding it.
747
  let litLast = false;
 
823
  const page = () => sheaf.pages[sheaf.index] ?? null;
824
 
825
  /**
826
+ * Has he looked at this page's contents? The one letter he carries is his
827
+ * orders, which he is reading when the game starts. A FRAGMENT CAN BE TAKEN
828
+ * BLIND and arrives unknown, and the whole gate at the end of the level
829
+ * turns on that difference.
830
  */
831
  const pageSeen = (pg) => {
832
  // A letter was read off a lit door and a key has nothing written on it, so
 
855
  * Put a page on the pile and open on it.
856
  *
857
  * FOUND ORDER means it goes on the end. Moving the index to it is what a
858
+ * person does with a handful of papers β€” the new one is the one in your
859
+ * hand β€” and it is what makes pinning one gesture instead of two: take a
860
+ * fragment off a body, walk to a door, press E, and it is already the page
861
+ * he is holding.
862
  */
863
  const filePage = (pg) => {
864
  sheaf.pages.push(pg);
 
873
  * explaining that he begins carrying nothing. That was true of a pile of
874
  * things he had FOUND. This is the one page he did not find: it is what he
875
  * was handed at the palace and what sent him down this road, and by the end
876
+ * of the village it sits under four fragments that, with the letters on the
877
+ * doors they answer, contradict every word of it. Nothing has to say so.
878
  *
879
  * IT IS AN ORDINARY LETTER PAGE. Not its own kind: it is a page of writing
880
  * with a building behind it, which is what every letter in this game is, and
 
894
 
895
  /**
896
  * What the band is showing, and it shows the document rather than describing
897
+ * it: the page he is holding up, or the letter on the wall in front of him.
898
+ *
899
+ * `key` says WHICH document, so the band can blank between two of them β€”
900
+ * a page turn, or looking from the door to his papers and back. See the band.
901
  */
902
+ const band = { text: null, alpha: 0, key: null };
903
 
904
  /**
905
  * WHAT THIS BUILD IS FOR, counted.
 
936
  sheetClip, mapClip, hurtClip, run, sheaf, band, textFor, letters, page, pageSeen, filedUnder,
937
  canMark: false,
938
  markHouse: null,
 
939
  canTakeKey: false,
940
  canPlaceKey: false,
941
  // Whether there is anything at his feet to pick up. Computed once here
 
943
  canTake: false,
944
  seed: SEED,
945
  houses: HOUSES, houseTint: PALETTE.WATCHERS,
946
+ houseClip, palaceClip, houseArt, doorPoint, keyPoint, letterPoint, slotPoint,
947
+ tree: TREE, treeClip, treeNoteDark, treePoint,
948
+ // What the band is reading off a wall right now, if anything: the house or
949
+ // the tree. Published for the checks, which ask whether it is the same
950
+ // door E would act on.
951
+ readingAt: null,
952
+ // The house E would act on, if any. Same door the band reads.
953
+ nearDoor: null,
954
+ village, gateX: GATE_X, gateStopX: GATE_STOP_X, gateRoomX: GATE_ROOM_X,
955
  villageGap: VILLAGE_GAP, bowlX0: BOWL.X0,
956
  flame: { clip: clips.fire, player: createPlayer(clips.fire), frame: 0 },
957
  torchArm: TORCH_ARM,
 
980
  state.clock = 0;
981
  for (const h of HOUSES) {
982
  h.seen = false; h.filed = null;
 
983
  h.open = false; h.settled = false; h.keyTaken = false;
984
  }
985
+ if (TREE) TREE.seen = false;
986
+ still = 0;
987
+ state.readingAt = null;
988
+ state.nearDoor = null;
989
  prevPickup = false;
990
  prince.attacking = false;
991
  prince.attackTime = 0;
 
1010
  sheaf.turns = 0;
1011
  band.text = null;
1012
  band.alpha = 0;
1013
+ band.key = null;
1014
  state.fade = 0;
1015
  // Deaths survive a restart. Everything else about the run does not β€” that
1016
  // is what "the findings are at stake" means.
 
1033
 
1034
  function update(dt, input) {
1035
  prince.prevX = prince.x;
1036
+ // Where he started this frame, for "is he standing still". Not prevX: a
1037
+ // hit and a shove both reset that to stop the draw smearing, so a knock
1038
+ // would look like standing still.
1039
+ const xFrom = prince.x;
1040
  torch.snapshot();
1041
  camera.snapshot();
1042
  state.clock += dt;
 
1149
  if (prince.x < 0) prince.x = 0;
1150
  // He does not hit a boundary, he hits a shut gate. Once it is up, the
1151
  // only thing left in his way is the man standing in it.
1152
+ //
1153
+ // AND THE GATEWAY IS HIS ONCE ITS KEEPER HAS GONE HOME. While the keeper
1154
+ // holds the post, the shut leaf stops him just inside the left post.
1155
+ // With the post empty he can walk the whole width of the leaf, under all
1156
+ // four sockets, to just inside the right one. Found in play: he stood
1157
+ // at the left edge and could not get close enough to see how many
1158
+ // sockets the gate wanted.
1159
+ const shut = keeperGone() ? GATE_ROOM_X : GATE_STOP_X;
1160
+ const stop = gate.open >= 1 ? Math.max(GATE_X + 260, shut) : shut;
1161
  if (prince.x > stop) prince.x = stop;
1162
  }
1163
+ camera.setBounds(0, keeperGone() ? GATE_ROOM_CAM_X : GATE_CAM_X);
1164
  camera.follow(prince.x);
1165
 
1166
  // --- The sword --------------------------------------------------------
 
1385
  // Lighting one is the only thing that says who lived here. It latches, the
1386
  // same way seeing an object on the ground latches, and for the same reason:
1387
  // he looked, and looking is not something he can un-do.
1388
+ //
1389
+ // THE DOOR OR ITS LETTER, EITHER ONE. The letter hangs left of the arch,
1390
+ // nearer him than the door, so there is a stretch of wall where the light
1391
+ // is on the letter and not yet on the door. If only the door counted he
1392
+ // could read a letter in the band and then find the door had never been
1393
+ // lit: the next read would not come free, and E would not pin.
1394
  for (const h of HOUSES) {
1395
  const [dx, dy] = doorPoint(h);
1396
+ const lp = letterPoint(h);
1397
+ if (torch.covers(dx, dy) || (lp && torch.covers(lp[0], lp[1]))) h.seen = true;
 
 
 
 
 
1398
  }
1399
  // THE NOTE ON THE TREE. No door to light first: the paper is all there is.
1400
+ if (TREE) {
1401
  const p = treePoint();
1402
+ if (p && torch.covers(p[0], p[1])) TREE.seen = true;
1403
  }
1404
 
1405
+ // THE DOOR HE IS AT, AND IT IS LATCHED RATHER THAN LIT.
1406
+ //
1407
+ // There used to be a `lit` house here, meaning "the torch is on this door
1408
+ // right now", and it was what marking tested β€” which only worked because
1409
+ // holding the sheaf forced the torch up. Seeing latches instead, and acting
1410
+ // on it only needs him to still be standing there.
1411
  //
1412
+ // ONE DOOR, AND THE BAND AND E BOTH USE IT. The band never reads him one
1413
+ // door while E acts on another. The doors are about 4200px apart and the
1414
+ // reach is 440, so there is never more than one to choose from anyway.
1415
+ let nearDoor = null;
1416
  for (const h of HOUSES) {
1417
+ if (!h.seen) continue;
1418
+ const p = slotPoint(h);
1419
+ if (p && Math.abs(prince.x - p[0]) <= TUNING.DOOR_REACH) { nearDoor = h; break; }
1420
  }
1421
+ // And the tree, on the same terms. It reads like a door and it never joins
1422
+ // nearDoor: there is nothing on it to pin to.
1423
+ let nearTree = null;
1424
+ if (TREE?.seen) {
1425
  const p = treePoint();
1426
+ if (p && Math.abs(prince.x - p[0]) <= TUNING.DOOR_REACH) nearTree = TREE;
1427
  }
1428
 
1429
  // AND THE KEY, WHICH NEEDS THE DOOR OPEN AND NOT THE TORCH UP.
 
1444
 
1445
  // --- the band -----------------------------------------------------------
1446
  //
1447
+ // IT SHOWS A DOCUMENT, VERBATIM, and a document is in one of two places: in
1448
+ // his hand, or nailed to the wall in front of him. Nothing else in this
1449
+ // game gets text: not doors, not the gate, not objects and not the prince.
1450
+ //
1451
+ // Q held the page in his hand. Wins
1452
+ // the torch on a letter, in reach that letter. The first read
1453
+ // standing still, in reach of a lit door that door's letter
1454
+ // otherwise closed
1455
  //
1456
+ // COMPARING IS LOOKING AT THE DOOR, THEN AT HIS PAPERS, THEN AT THE DOOR.
1457
+ // Hold Q and the band reads the fragment in his hand; let go and it reads
1458
+ // the door again. That is the whole deduction, stood in one place.
1459
  //
1460
+ // THE FIRST READ NEEDS LIGHT, AND ONLY THE FIRST. The first draft had every
1461
+ // read need the torch, so comparing was SPACE held with Q tapped in
1462
+ // between. On a phone TORCH and PAGES are both on the left wheel and one
1463
+ // thumb cannot hold one while pressing the other β€” the trap taking a letter
1464
+ // fell into once already. So a door he has lit reads again for nothing,
1465
+ // and comparing is one button: PAGES down, PAGES up.
1466
+ //
1467
+ // STANDING STILL is what stops it opening at every door he walks past. Not
1468
+ // walking, not swinging and not knocked back, for READ_STILL_MS. Holding Q
1469
+ // counts β€” he cannot walk with it up β€” so letting go of Q goes straight
1470
+ // back to the door.
1471
+ const stood = !prince.moving && !prince.attacking && prince.x === xFrom;
1472
+ still = stood ? still + dt * 1000 : 0;
1473
+ const readAt = nearDoor ?? nearTree;
1474
+ const readPoint = readAt === TREE ? treePoint() : readAt ? letterPoint(readAt) : null;
1475
+ const reading = !!readAt &&
1476
+ ((!!readPoint && torch.covers(readPoint[0], readPoint[1])) ||
1477
+ still >= TUNING.READ_STILL_MS);
1478
+ state.readingAt = sheaf.up || !reading ? null : readAt;
1479
  // Gated on the raise so the band and the page arrive together β€” the sheet
1480
  // itself is not drawn until the arm is up, and a band that beat it there
1481
  // would read as a subtitle rather than as the paper in his hand.
1482
+ //
1483
+ // THE SHEAF WINS FROM THE MOMENT Q GOES DOWN, not from when the page is
1484
+ // up. For the quarter-second of the raise the band is blank: he has looked
1485
+ // away from the wall and not yet down at his hand.
1486
  const pg = sheaf.up && torch.raise > 0.9 ? page() : null;
1487
  // AN UNKNOWN PAGE HAS NOTHING ON IT YET. He took it blind and he has not
1488
  // looked; the band would be reading him words he cannot have.
1489
+ const want = sheaf.up ? (pg && pageSeen(pg) ? textFor(pg) : null)
1490
+ : state.readingAt ? textFor({ kind: 'letter', house: state.readingAt.index })
 
 
 
 
 
 
1491
  : null;
1492
 
1493
+ // CLEARED AND REFILLED EVERY TIME IT CHANGES DOCUMENT, never swapped in
1494
+ // place. The blank between two pages is a fraction of a second and it is
1495
+ // the entire reason a turn reads as a turn β€” swap the text and two letters
1496
+ // in a row look like one letter changing its mind. Looking from the door to
1497
+ // the page in his hand is the same thing: two pieces of paper, not one
1498
+ // label flipping.
1499
+ //
1500
+ // A page is keyed by the turn count rather than by its words, because
1501
+ // paging from one page to another with the SAME words has to still read as
1502
+ // a turn.
1503
+ const key = !want ? null : pg ? `page ${sheaf.turns}` : `wall ${state.readingAt.index}`;
1504
+ if (key !== band.key) { band.key = key; if (key) band.alpha = 0; }
1505
  band.text = want;
1506
  band.alpha = Math.min(1, Math.max(0, band.alpha + (want ? dt * 5 : -dt * 9)));
1507
 
 
1511
  // more: what goes under a plot is a claim he makes, and a claim only means
1512
  // something if it can be wrong.
1513
  //
1514
+ // THE ACT IS THE SAME POSTURE AS EVERYTHING ELSE. He stands at a door with
1515
+ // a piece of paper in his hand and presses take. No menu, no drag, no
1516
+ // assignment screen β€” and no new button, because TAKE is already the
1517
+ // contextual one and pinning is "put this where it goes".
1518
  //
1519
+ // The door has to have been LIT ONCE β€” the same latch that reads it again
1520
+ // for free β€” and he has to be standing at it. Not lit now and not with Q
1521
+ // held: asking for both hands is the problem the hand addendum fixed.
1522
  // ONE ACT PER PRESS, DECIDED HERE AND NOT UPSTREAM.
1523
  //
1524
  // input.js already makes `pickup` a one-shot, and that was enough while
 
1553
  const handPage = page();
1554
  state.handPage = handPage;
1555
 
1556
+ // THE SLOT TAKES A PIN FROM THE FIRST LIGHT. It used to wait for the letter
1557
+ // to come off: the patch was earned, and taking the letter was what showed
1558
+ // him where a fragment goes. The letter never comes off now, and the slot
1559
+ // is beside it from the start β€” a bare nail under a letter says something
1560
+ // goes here.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1561
  //
1562
  // AND NOT AT A DOOR THAT IS FINISHED WITH. Someone walked home to it; the
1563
+ // slot has the only thing it was ever going to hold.
1564
+ const patchTakes = !!nearDoor && !gate.through && !nearDoor.settled;
 
 
 
 
1565
  const holding = handPage?.kind === 'fragment' ? handPage.kept : null;
1566
 
1567
  // PIN, or TAKE BACK, and which one is decided by what is already there.
 
1577
  const canPin = patchTakes && holding !== null && holding !== nearDoor.filed;
1578
  const canMark = canPin || canRetrieve;
1579
 
1580
+ // THE SLOT, THEN THE KEY. A claim about a door comes before the thing that
1581
+ // opens the gate: evidence before conclusions before rewards, which is the
1582
+ // order this whole game is arguing for.
1583
+ //
1584
+ // READING IS NOT ON THIS LIST. What the band shows decides nothing about E.
1585
+ // It used to β€” a letter in reach switched off pinning until it was taken β€”
1586
+ // and with letters never taken that would have switched pinning off at
1587
+ // exactly the moment he reads the letter he is trying to answer.
1588
+ state.canMark = canMark;
1589
+ state.canPin = canPin;
1590
+ state.nearDoor = nearDoor;
1591
  // WHICH house it would go under. The sheet draws that house's plot beside
1592
  // the page when this is set, so the paper itself says "this one, here".
1593
  state.markHouse = state.canPin ? nearDoor.index : null;
1594
+ state.canTakeKey = !canMark && !!litKey;
1595
  // AND PUTTING ONE IN. He stops 380px short of the leaf and can never get
1596
  // closer, so the reach is the door's, not the ground's.
1597
  state.canPlaceKey = !canMark && prince.keys > 0 && !gate.through &&
 
1676
  // second time to decide whether to light the button, which is two places
1677
  // that have to agree about what is in reach.
1678
  state.canTake = !!nearest;
1679
+ if (wantsPickup && state.canPlaceKey && !prince.attacking) {
 
 
 
 
 
 
 
 
 
1680
  // THE SAME POSTURE AS EVERYTHING ELSE. He stands at the thing and
1681
  // presses take. Four times, one socket each, and the leaf lifts on the
1682
  // fourth β€” no ceremony, no confirmation, and nothing said about the
 
1773
  if (gate.filled >= TUNING.GATE_WANTS && gate.open < 1) {
1774
  gate.open = Math.min(1, gate.open + (dt * 1000) / TUNING.GATE_OPEN_MS);
1775
  }
1776
+ // THROUGH IS A STEP, not a place. He can be standing inside the gateway
1777
+ // when the leaf lifts β€” it is his to walk once the keeper is home β€” and
1778
+ // the level must not end on him for standing still under it.
1779
+ if (gate.open >= 1 && prince.x >= GATE_X - 40 && prince.moving && prince.facing > 0) {
1780
  // Through it, and the playthrough is over. No prompt, no score screen,
1781
  // and no mention of the empty slot. The gate wanted four. It got four.
1782
  gate.through = true;
src/tuning.js CHANGED
@@ -511,11 +511,18 @@ export const TUNING = {
511
  // 225px out from the body, and he stops about 170px short of it when he
512
  // fights, so this makes him walk the last stretch on purpose.
513
  PICKUP_REACH: 120,
514
- // How close he has to be to a door to take the letter off its wall, once he
515
- // has looked at it. Wider than PICKUP_REACH because the letter is up on the
516
  // wall rather than at his feet, and wide enough to cover anywhere he could
517
  // have lit it from β€” he reads it standing about 300px off.
518
  DOOR_REACH: 440,
 
 
 
 
 
 
 
519
 
520
  // The gate wants four objects he has LOOKED at. Not four objects.
521
  GATE_WANTS: 4,
@@ -833,6 +840,8 @@ const OVERRIDABLE = new Set([
833
  'DOWN_MS', 'RISE_MS', 'STAGGER_MS', 'HITS_TO_DOWN',
834
  'WALK_SPEED', 'DRAINED_SPEED', 'RUSH_SPEED', 'HOME_SPEED', 'REVEAL_MS', 'START_AT',
835
  'ORDER_HOLD_MS',
 
 
836
  // The two lanes. Every one of these is a start value that gets tuned by eye.
837
  'BACK_LANE_SCALE', 'BACK_LANE_RISE', 'BACK_LANE_VALUE', 'STEP_MS', 'WATCH_NEAR', 'WATCH_FAR', 'STIR_TREMBLE_PX', 'STIR_TREMBLE_FROM',
838
  // The V1 judgement. Sweeping it has to be a query string, not an edit.
 
511
  // 225px out from the body, and he stops about 170px short of it when he
512
  // fights, so this makes him walk the last stretch on purpose.
513
  PICKUP_REACH: 120,
514
+ // How close he has to be to a door to read its letter or pin to its slot,
515
+ // once he has lit it. Wider than PICKUP_REACH because both are up on the
516
  // wall rather than at his feet, and wide enough to cover anywhere he could
517
  // have lit it from β€” he reads it standing about 300px off.
518
  DOOR_REACH: 440,
519
+ // How long he has to stand still at a door he has lit before the band reads
520
+ // it to him again. The first read needs the torch; every read after that is
521
+ // free, or comparing a door with the page in his hand takes two buttons, and
522
+ // on a phone both are under one thumb. Standing still is what stops it
523
+ // opening at every door he walks past β€” and the wait is what stops it
524
+ // flashing half open between two swings of the sword.
525
+ READ_STILL_MS: 300,
526
 
527
  // The gate wants four objects he has LOOKED at. Not four objects.
528
  GATE_WANTS: 4,
 
840
  'DOWN_MS', 'RISE_MS', 'STAGGER_MS', 'HITS_TO_DOWN',
841
  'WALK_SPEED', 'DRAINED_SPEED', 'RUSH_SPEED', 'HOME_SPEED', 'REVEAL_MS', 'START_AT',
842
  'ORDER_HOLD_MS',
843
+ // Letters stay on the door. How long standing still takes to read one again.
844
+ 'READ_STILL_MS',
845
  // The two lanes. Every one of these is a start value that gets tuned by eye.
846
  'BACK_LANE_SCALE', 'BACK_LANE_RISE', 'BACK_LANE_VALUE', 'STEP_MS', 'WATCH_NEAR', 'WATCH_FAR', 'STIR_TREMBLE_PX', 'STIR_TREMBLE_FROM',
847
  // The V1 judgement. Sweeping it has to be a query string, not an edit.