Spaces:
Running
Running
77dd24d: A source check that checks, and a dev server that answers
Browse filesPublished from cloudronin/astara@77dd24d4b0f0c24be9c7a520ae4810a52ee8def9
- README.md +9 -6
- assets/origin_tree/lit_00.svg +0 -6
- manifest.json +44 -31
- src/assets.js +5 -4
- src/checks.js +702 -234
- src/input.js +4 -3
- src/main.js +1 -1
- src/render.js +71 -104
- src/scene_level.js +204 -156
- 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 |
-
**
|
| 37 |
-
|
| 38 |
-
the door
|
|
|
|
|
|
|
|
|
|
| 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
|
| 47 |
-
|
| 48 |
-
|
| 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 |
-
"
|
| 971 |
[
|
| 972 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
704,
|
| 974 |
56,
|
| 975 |
76
|
| 976 |
],
|
| 977 |
[
|
| 978 |
-
|
| 979 |
704,
|
| 980 |
56,
|
| 981 |
76
|
| 982 |
],
|
| 983 |
[
|
| 984 |
-
|
| 985 |
704,
|
| 986 |
56,
|
| 987 |
76
|
| 988 |
],
|
| 989 |
[
|
| 990 |
-
|
| 991 |
704,
|
| 992 |
56,
|
| 993 |
76
|
| 994 |
],
|
| 995 |
[
|
| 996 |
-
|
| 997 |
704,
|
| 998 |
56,
|
| 999 |
76
|
| 1000 |
]
|
| 1001 |
],
|
| 1002 |
-
"
|
|
|
|
| 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
|
| 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 |
-
"
|
| 1077 |
[
|
| 1078 |
185,
|
| 1079 |
984,
|
|
@@ -1081,27 +1114,7 @@
|
|
| 1081 |
76
|
| 1082 |
]
|
| 1083 |
],
|
| 1084 |
-
"
|
| 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
|
| 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 |
-
//
|
| 199 |
-
// the
|
| 200 |
-
//
|
| 201 |
-
|
|
|
|
| 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 |
-
*
|
| 719 |
-
* no grouping, no sorting, no index and no tabs, because all of those
|
| 720 |
-
* game telling him what he has, and what he has is the thing he is
|
| 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.
|
| 740 |
-
//
|
| 741 |
-
//
|
| 742 |
-
//
|
| 743 |
-
|
| 744 |
-
s.
|
| 745 |
-
|
| 746 |
-
const
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 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.
|
| 786 |
-
//
|
| 787 |
-
//
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
const foundOrder =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 797 |
sheaf.index = 0;
|
| 798 |
const before = sheaf.turns;
|
| 799 |
drive(1, { sheaf: true, right: true });
|
| 800 |
const turnsPerSecond = sheaf.turns - before;
|
| 801 |
|
| 802 |
-
//
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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 &&
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
stopsAtTheEnd && stopsAtTheStart && reopensWhereHeLeftIt &&
|
| 845 |
blankWhileUnknown && resolvedOnPage && resolvedTheOneHeIsOn &&
|
| 846 |
opensWhenHeTurnsToIt,
|
| 847 |
-
startsWithTheOrder,
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
takenOnceOnly: takenOnce,
|
| 851 |
-
takeableWithTheTorchDown: offeredInTheDark && takenInTheDark,
|
| 852 |
-
butNotFromAcrossTheVillage: notFromAcrossTheVillage,
|
| 853 |
holdsAtTheFrontOfThePile: holdsAtTheFront,
|
| 854 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 1210 |
-
*
|
| 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].
|
| 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 |
-
//
|
| 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 |
-
//
|
| 1333 |
-
|
| 1334 |
-
// the
|
| 1335 |
-
// offered, which is the latch, not a mode
|
| 1336 |
-
drive(1.4, { torch: true });
|
| 1337 |
stand(H, withQ);
|
| 1338 |
-
|
| 1339 |
-
const tookLetter = h.letterTaken && h.filed == null;
|
| 1340 |
|
| 1341 |
-
// 2. A bare
|
| 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,
|
| 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.
|
| 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.
|
| 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
|
| 1496 |
//
|
| 1497 |
-
//
|
| 1498 |
-
//
|
| 1499 |
-
//
|
| 1500 |
-
//
|
| 1501 |
-
//
|
| 1502 |
-
//
|
| 1503 |
//
|
| 1504 |
// Measured over one full cycle of KEPT_PULSE_HZ, on a box wider than
|
| 1505 |
-
// the
|
| 1506 |
const wide = (h) => {
|
| 1507 |
-
const [nx, ny, nw, nh] = clip.
|
| 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
|
| 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
|
| 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 |
-
|
| 1568 |
struck && quiet && gone,
|
| 1569 |
flickerMovesTheMeanBy: noise,
|
| 1570 |
-
|
| 1571 |
-
|
| 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 |
-
//
|
| 1727 |
-
//
|
| 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 |
-
|
|
|
|
| 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 &&
|
| 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
|
| 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
|
|
|
|
| 1987 |
//
|
| 1988 |
-
//
|
| 1989 |
-
//
|
| 1990 |
-
//
|
| 1991 |
-
//
|
| 1992 |
-
//
|
| 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
|
| 2000 |
-
const litNotRead = unread.seen && !unread.letterTaken;
|
| 2001 |
mark();
|
| 2002 |
-
|
| 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 &&
|
| 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 |
-
|
| 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
|
| 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
|
| 2314 |
-
*
|
| 2315 |
-
*
|
|
|
|
| 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
|
| 2324 |
const clip = s.houseClip;
|
| 2325 |
-
const [fx, fy, fw, fh] =
|
| 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
|
| 2357 |
-
//
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 2414 |
-
//
|
| 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:
|
| 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 |
-
|
| 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
|
| 2521 |
-
|
| 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
|
|
|
|
| 2530 |
const armedPages = s.sheaf.pages.length;
|
| 2531 |
-
|
| 2532 |
-
|
| 2533 |
-
s.prince.x = s.prince.prevX = 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 &&
|
|
|
|
| 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 |
-
*
|
| 2825 |
-
*
|
| 2826 |
-
*
|
| 2827 |
-
*
|
|
|
|
| 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
|
|
|
|
| 2841 |
s.prince.x = s.prince.prevX = t.x - 300;
|
| 2842 |
s.prince.facing = 1;
|
| 2843 |
s.camera.reset(s.prince.x);
|
| 2844 |
-
drive(
|
| 2845 |
-
const unreadInTheDark = !t.
|
| 2846 |
|
| 2847 |
// Lit: the band reads it, and nobody wakes.
|
| 2848 |
drive(1.4, { torch: true });
|
| 2849 |
-
const reads = t.
|
| 2850 |
const nobodyWoke = s.drained.every((d) => !d.revealed);
|
| 2851 |
|
| 2852 |
-
//
|
| 2853 |
-
|
| 2854 |
-
|
| 2855 |
-
|
| 2856 |
-
|
| 2857 |
-
|
| 2858 |
-
|
| 2859 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2860 |
|
| 2861 |
-
//
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2868 |
|
| 2869 |
scene.reset();
|
| 2870 |
-
const
|
| 2871 |
return {
|
| 2872 |
name: 'the tree teaches',
|
| 2873 |
-
pass: !!note && placed && unreadInTheDark && reads && nobodyWoke &&
|
| 2874 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
*
|
| 170 |
-
*
|
| 171 |
-
*
|
|
|
|
| 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.
|
| 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
|
| 270 |
-
* nail, drawn by drawNail like every
|
| 271 |
-
*
|
| 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,
|
| 284 |
-
} else if (
|
| 285 |
const art = s.letterArt;
|
| 286 |
-
const b = nailBox(clip
|
| 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
|
| 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
|
| 620 |
-
*
|
| 621 |
-
*
|
| 622 |
-
*
|
| 623 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 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.
|
| 959 |
-
//
|
| 960 |
-
//
|
| 961 |
-
//
|
| 962 |
-
const
|
| 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
|
| 1128 |
*
|
| 1129 |
-
*
|
| 1130 |
-
*
|
| 1131 |
-
*
|
| 1132 |
-
*
|
| 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
|
| 1149 |
*
|
| 1150 |
-
*
|
| 1151 |
-
*
|
| 1152 |
-
*
|
| 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
|
| 1163 |
-
function nailBox(
|
| 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
|
| 1177 |
-
*
|
| 1178 |
-
*
|
| 1179 |
*
|
| 1180 |
-
* LIT PASS ONLY, so all
|
| 1181 |
* else in a doorway.
|
|
|
|
|
|
|
|
|
|
| 1182 |
*/
|
| 1183 |
-
function drawNail(c, s, clip, house, x, y,
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 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(
|
| 1197 |
-
Math.round(
|
| 1198 |
-
c.drawImage(
|
| 1199 |
-
|
| 1200 |
-
return;
|
| 1201 |
}
|
|
|
|
| 1202 |
|
| 1203 |
-
// 2. THE
|
| 1204 |
-
// made it, because paper curls and the weather got in at the
|
| 1205 |
-
|
|
|
|
|
|
|
| 1206 |
const [nx, ny, nw, nh] = rect;
|
| 1207 |
const px0 = Math.round(x + nx);
|
| 1208 |
const py0 = Math.round(y + ny);
|
| 1209 |
|
| 1210 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 (
|
| 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(
|
| 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(
|
| 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.
|
| 1840 |
-
: s.
|
| 1841 |
-
:
|
| 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 |
-
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 343 |
-
*
|
| 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?.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 398 |
-
// about: light it
|
| 399 |
-
//
|
|
|
|
| 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
|
| 407 |
const treePoint = () => {
|
| 408 |
-
const r = treeClip?.
|
| 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 |
-
*
|
| 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?
|
| 797 |
-
*
|
| 798 |
-
* the whole gate at the end of the level
|
|
|
|
| 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
|
| 828 |
-
* hand β and it is what makes
|
| 829 |
-
*
|
| 830 |
-
*
|
| 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
|
| 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
|
| 867 |
-
*
|
|
|
|
|
|
|
| 868 |
*/
|
| 869 |
-
const band = { text: null, alpha: 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,
|
| 916 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 1336 |
-
//
|
| 1337 |
-
//
|
| 1338 |
-
//
|
| 1339 |
-
//
|
| 1340 |
-
|
| 1341 |
for (const h of HOUSES) {
|
| 1342 |
const [dx, dy] = doorPoint(h);
|
| 1343 |
-
|
| 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
|
| 1353 |
const p = treePoint();
|
| 1354 |
-
if (p && torch.covers(p[0], p[1]))
|
| 1355 |
}
|
| 1356 |
|
| 1357 |
-
//
|
| 1358 |
-
//
|
| 1359 |
-
//
|
|
|
|
|
|
|
|
|
|
| 1360 |
//
|
| 1361 |
-
//
|
| 1362 |
-
//
|
| 1363 |
-
//
|
| 1364 |
-
|
| 1365 |
for (const h of HOUSES) {
|
| 1366 |
-
if (
|
| 1367 |
-
const p =
|
| 1368 |
-
if (p && Math.abs(prince.x - p[0]) <= TUNING.DOOR_REACH) {
|
| 1369 |
}
|
| 1370 |
-
// And the tree
|
| 1371 |
-
//
|
| 1372 |
-
|
|
|
|
| 1373 |
const p = treePoint();
|
| 1374 |
-
if (p && Math.abs(prince.x - p[0]) <= TUNING.DOOR_REACH)
|
| 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
|
| 1396 |
-
//
|
| 1397 |
-
//
|
| 1398 |
-
//
|
| 1399 |
-
// the
|
| 1400 |
-
//
|
|
|
|
|
|
|
| 1401 |
//
|
| 1402 |
-
//
|
| 1403 |
-
//
|
|
|
|
| 1404 |
//
|
| 1405 |
-
//
|
| 1406 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
| 1424 |
-
// between two pages is a fraction of a second and it is
|
| 1425 |
-
// turn reads as a turn β swap the text and two letters
|
| 1426 |
-
// one letter changing its mind.
|
| 1427 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 1439 |
-
//
|
| 1440 |
-
//
|
| 1441 |
//
|
| 1442 |
-
// The door has to
|
| 1443 |
-
//
|
| 1444 |
-
//
|
| 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
|
| 1480 |
-
//
|
| 1481 |
-
//
|
| 1482 |
-
//
|
| 1483 |
-
//
|
| 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 |
-
//
|
| 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 |
-
//
|
| 1531 |
-
//
|
| 1532 |
-
//
|
| 1533 |
-
//
|
| 1534 |
-
|
| 1535 |
-
|
| 1536 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = !
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 515 |
-
//
|
| 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.
|