Spaces:
Running
Running
eb6e8a3: The page in his hand
Browse filesPublished from cloudronin/astara@eb6e8a38ae57ea8cfc87ca5cfaa67c067ec17d87
- index.html +8 -15
- src/checks.js +221 -10
- src/input.js +14 -21
- src/main.js +1 -1
- src/render.js +70 -5
- src/scene_level.js +88 -34
index.html
CHANGED
|
@@ -57,20 +57,13 @@
|
|
| 57 |
#pad .walkwheel { width:176px; height:176px; }
|
| 58 |
#pad .walkwheel::before { inset:38px; }
|
| 59 |
|
| 60 |
-
/* MARK
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
thumb
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
same kind of thing: the verb that depends on where he is standing. It is
|
| 68 |
-
there only while there is something to file. */
|
| 69 |
-
#pad #markbtn {
|
| 70 |
-
opacity:0; pointer-events:none; transition:opacity .14s linear;
|
| 71 |
-
color:rgba(255,220,180,.9); border-color:rgba(255,220,180,.55);
|
| 72 |
-
}
|
| 73 |
-
#pad #markbtn.live { opacity:1; pointer-events:auto; }
|
| 74 |
/* Six o'clock, holding the wheel's shape open until there is a fourth thing
|
| 75 |
to do with the right thumb. Solid and small — a dashed circle reads as
|
| 76 |
something broken rather than something reserved. */
|
|
@@ -188,7 +181,7 @@
|
|
| 188 |
</div>
|
| 189 |
<div class="cluster right">
|
| 190 |
<div class="wheel walkwheel">
|
| 191 |
-
<button type="button" class="n"
|
| 192 |
<button type="button" class="w arrow" data-act="left" aria-label="walk left">◀</button>
|
| 193 |
<button type="button" class="e arrow" data-act="right" aria-label="walk right">▶</button>
|
| 194 |
<button type="button" class="s empty" tabindex="-1" aria-hidden="true"></button>
|
|
|
|
| 57 |
#pad .walkwheel { width:176px; height:176px; }
|
| 58 |
#pad .walkwheel::before { inset:38px; }
|
| 59 |
|
| 60 |
+
/* MARK WAS HERE AND IS GONE. It existed because pinning a page needed PAGES
|
| 61 |
+
held and TAKE pressed, and both of those live on the left wheel — one thumb
|
| 62 |
+
cannot hold one and press the other, so the act needed a button under the
|
| 63 |
+
other thumb. The top page of the pile is now what he is holding and TAKE
|
| 64 |
+
acts on it wherever he is standing, so there is no hold, no second button,
|
| 65 |
+
and nothing for this side to do but walk. The workaround outlived its
|
| 66 |
+
problem by one build. */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
/* Six o'clock, holding the wheel's shape open until there is a fourth thing
|
| 68 |
to do with the right thumb. Solid and small — a dashed circle reads as
|
| 69 |
something broken rather than something reserved. */
|
|
|
|
| 181 |
</div>
|
| 182 |
<div class="cluster right">
|
| 183 |
<div class="wheel walkwheel">
|
| 184 |
+
<button type="button" class="n empty" tabindex="-1" aria-hidden="true"></button>
|
| 185 |
<button type="button" class="w arrow" data-act="left" aria-label="walk left">◀</button>
|
| 186 |
<button type="button" class="e arrow" data-act="right" aria-label="walk right">▶</button>
|
| 187 |
<button type="button" class="s empty" tabindex="-1" aria-hidden="true"></button>
|
src/checks.js
CHANGED
|
@@ -1177,6 +1177,166 @@ export function createChecks({ renderer, scene, canvas, clips = {} }) {
|
|
| 1177 |
drive(0.15, extra);
|
| 1178 |
}
|
| 1179 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1180 |
function walksHome() {
|
| 1181 |
const carriers = armed();
|
| 1182 |
const owner = carriers.find((d) => d.kept === 0); // the forge's
|
|
@@ -1607,16 +1767,39 @@ export function createChecks({ renderer, scene, canvas, clips = {} }) {
|
|
| 1607 |
mark();
|
| 1608 |
const needsADoor = filed().join(',') === before;
|
| 1609 |
|
| 1610 |
-
// 6. AND NOT
|
| 1611 |
-
//
|
| 1612 |
-
|
| 1613 |
-
|
| 1614 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1615 |
// A LETTER IS NOT A CLAIM. Only a torn page off a body can be filed — the
|
| 1616 |
// letter is what the house already says about itself.
|
| 1617 |
s.sheaf.index = s.sheaf.pages.findIndex((p) => p.kind === 'letter');
|
| 1618 |
at(0); mark();
|
| 1619 |
-
const mapCannotBeFiled = s.sheaf.index < 0 || filed().join(',') ===
|
| 1620 |
|
| 1621 |
// 7. NO FEEDBACK, EVER. Nothing in the state says right or wrong, and an
|
| 1622 |
// empty plot is not flagged. Four filed and one empty is the finding,
|
|
@@ -1634,17 +1817,20 @@ export function createChecks({ renderer, scene, canvas, clips = {} }) {
|
|
| 1634 |
|
| 1635 |
return {
|
| 1636 |
name: 'marking is a claim',
|
| 1637 |
-
pass: nothingAutoFiles && needsTheLetterOff &&
|
| 1638 |
afterFirst[0] === 2 && afterReplace[0] === 3 &&
|
| 1639 |
afterMove[0] == null && afterMove[1] === 3 && onlyOnce &&
|
| 1640 |
-
needsADoor &&
|
| 1641 |
frozenAtTheGate,
|
| 1642 |
nothingFilesItself: nothingAutoFiles,
|
| 1643 |
andThereIsNoSlotUntilTheLetterIsOff: needsTheLetterOff,
|
| 1644 |
theSamePressTakesTheLetterInstead: tookTheLetterInstead,
|
| 1645 |
afterFirst, afterReplace, afterMove,
|
| 1646 |
oneFragmentOneHouse: onlyOnce,
|
| 1647 |
-
needsALitDoor: needsADoor,
|
|
|
|
|
|
|
|
|
|
| 1648 |
aLetterCannotBeFiled: mapCannotBeFiled,
|
| 1649 |
noRightOrWrongAnywhere: noVerdict,
|
| 1650 |
frozenAtTheGate,
|
|
@@ -1811,15 +1997,38 @@ export function createChecks({ renderer, scene, canvas, clips = {} }) {
|
|
| 1811 |
return frame();
|
| 1812 |
});
|
| 1813 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1814 |
return {
|
| 1815 |
name: 'the keys are shown',
|
| 1816 |
pass: withoutTheReadout > 1500 && withTheReadout > 1500 &&
|
| 1817 |
-
whileReading < withoutTheReadout * 0.25
|
|
|
|
| 1818 |
inkWithTheReadoutOff: withoutTheReadout,
|
| 1819 |
inkWithItOn: withTheReadout,
|
| 1820 |
inkWhileReadingALetter: whileReading,
|
| 1821 |
survivesDebugBeingOff: withoutTheReadout > 1500,
|
| 1822 |
getsOutOfTheWayOfAPage: whileReading < withoutTheReadout * 0.25,
|
|
|
|
| 1823 |
};
|
| 1824 |
}
|
| 1825 |
|
|
@@ -2022,6 +2231,7 @@ export function createChecks({ renderer, scene, canvas, clips = {} }) {
|
|
| 2022 |
doorsRead(), pagesCost(), sheafPages(), markingIsAClaim(),
|
| 2023 |
theRoomAsks(), aGlanceIsFree(), theLightGoesOut(), theKeysAreShown(), bandFits(),
|
| 2024 |
dealObeysRules(), swingerIsBait(),
|
|
|
|
| 2025 |
walksHome(), keyBehindTheDoor(), residueStaysLocked(),
|
| 2026 |
nothingOutOfReach(), healthCosts(), theGate()];
|
| 2027 |
return { pass: out.every((r) => r.pass), results: out };
|
|
@@ -2030,6 +2240,7 @@ export function createChecks({ renderer, scene, canvas, clips = {} }) {
|
|
| 2030 |
return { figureContrast, watcherContrast, brightMass, skyWeave,
|
| 2031 |
bowlLayout, bodiesPersist, wholeLevelWalkable, stillUntilLit, doorsRead,
|
| 2032 |
pagesCost, sheafPages, markingIsAClaim, theRoomAsks, aGlanceIsFree, theLightGoesOut, theKeysAreShown, bandFits, dealObeysRules, swingerIsBait,
|
|
|
|
| 2033 |
walksHome, keyBehindTheDoor, residueStaysLocked,
|
| 2034 |
nothingOutOfReach, healthCosts, theGate,
|
| 2035 |
all, frameAt, frameAtX };
|
|
|
|
| 1177 |
drive(0.15, extra);
|
| 1178 |
}
|
| 1179 |
|
| 1180 |
+
/**
|
| 1181 |
+
* THE TOP PAGE IS WHAT IS IN HIS HAND, and it stays there when the pile
|
| 1182 |
+
* goes away.
|
| 1183 |
+
*
|
| 1184 |
+
* The sheaf is for CHOOSING. Letting go of Q is putting the rest of the
|
| 1185 |
+
* papers back in your coat, not putting down the one you picked.
|
| 1186 |
+
*/
|
| 1187 |
+
function topPageIsTheHand() {
|
| 1188 |
+
armed();
|
| 1189 |
+
const held = () => s.handPage;
|
| 1190 |
+
|
| 1191 |
+
// Page to a fragment and let go.
|
| 1192 |
+
s.sheaf.index = s.sheaf.pages.findIndex((p) => p.kind === 'fragment');
|
| 1193 |
+
drive(0.6, { sheaf: true });
|
| 1194 |
+
const upIsFragment = held()?.kind === 'fragment';
|
| 1195 |
+
drive(0.6); // Q released
|
| 1196 |
+
const stillFragment = held()?.kind === 'fragment' && !s.sheaf.up;
|
| 1197 |
+
|
| 1198 |
+
// A letter is a different shape in the hand, and it can be on top too —
|
| 1199 |
+
// but one has to be in the pile first, and armed() only takes fragments.
|
| 1200 |
+
s.prince.x = s.prince.prevX = s.houses[2].x - 300;
|
| 1201 |
+
s.prince.facing = 1;
|
| 1202 |
+
s.camera.reset(s.prince.x);
|
| 1203 |
+
drive(1.4, { torch: true });
|
| 1204 |
+
drive(0.2, { pickup: true });
|
| 1205 |
+
drive(0.15);
|
| 1206 |
+
const li = s.sheaf.pages.findIndex((p) => p.kind === 'letter');
|
| 1207 |
+
s.sheaf.index = li;
|
| 1208 |
+
drive(0.6);
|
| 1209 |
+
const letterOnTop = held()?.kind === 'letter';
|
| 1210 |
+
|
| 1211 |
+
// AND TAKING SOMETHING PUTS IT ON TOP, which is the whole tutorial: knock
|
| 1212 |
+
// one down, take the scrap, walk to the door, press E. No Q anywhere.
|
| 1213 |
+
const c = s.drained.find((d) => d.kept != null && !d.keptTaken)
|
| 1214 |
+
?? s.drained.find((d) => d.kept != null);
|
| 1215 |
+
const before = s.sheaf.pages.length;
|
| 1216 |
+
s.prince.x = s.prince.prevX = s.houses[0].x - 900;
|
| 1217 |
+
s.camera.reset(s.prince.x);
|
| 1218 |
+
s.sheaf.pages.push({ kind: 'fragment', kept: 3 });
|
| 1219 |
+
s.sheaf.index = s.sheaf.pages.length - 1;
|
| 1220 |
+
drive(0.3);
|
| 1221 |
+
const takingPutsItOnTop = s.sheaf.pages.length === before + 1 &&
|
| 1222 |
+
held()?.kept === 3;
|
| 1223 |
+
|
| 1224 |
+
// An empty pile is an empty hand.
|
| 1225 |
+
s.sheaf.pages.length = 0;
|
| 1226 |
+
s.sheaf.index = 0;
|
| 1227 |
+
drive(0.3);
|
| 1228 |
+
const emptyIsEmpty = held() == null;
|
| 1229 |
+
|
| 1230 |
+
return {
|
| 1231 |
+
name: 'the top page is the hand',
|
| 1232 |
+
pass: upIsFragment && stillFragment && letterOnTop &&
|
| 1233 |
+
takingPutsItOnTop && emptyIsEmpty && c != null,
|
| 1234 |
+
aFragmentSurvivesTheReleaseOfQ: stillFragment,
|
| 1235 |
+
aLetterCanBeOnTopToo: letterOnTop,
|
| 1236 |
+
takingPutsItOnTop, andNothingHeldWithAnEmptyPile: emptyIsEmpty,
|
| 1237 |
+
};
|
| 1238 |
+
}
|
| 1239 |
+
|
| 1240 |
+
/**
|
| 1241 |
+
* WHAT `E` DOES AT A DOOR, every row of it, and Q makes no difference to any
|
| 1242 |
+
* of them.
|
| 1243 |
+
*
|
| 1244 |
+
* One button, no modes. It is take-or-leave-the-thing-in-front-of-you, which
|
| 1245 |
+
* is what E has meant everywhere else in this game since the first object on
|
| 1246 |
+
* the ground.
|
| 1247 |
+
*/
|
| 1248 |
+
function eAtTheDoor() {
|
| 1249 |
+
const rows = [];
|
| 1250 |
+
const put = (k) => {
|
| 1251 |
+
s.sheaf.index = k === null
|
| 1252 |
+
? s.sheaf.pages.findIndex((p) => p.kind === 'letter')
|
| 1253 |
+
: s.sheaf.pages.findIndex((p) => p.kind === 'fragment' && p.kept === k);
|
| 1254 |
+
};
|
| 1255 |
+
const stand = (i, withQ) => {
|
| 1256 |
+
s.prince.x = s.prince.prevX = s.houses[i].x - 300;
|
| 1257 |
+
s.prince.facing = 1;
|
| 1258 |
+
s.camera.reset(s.prince.x);
|
| 1259 |
+
drive(0.6, withQ ? { sheaf: true } : {});
|
| 1260 |
+
};
|
| 1261 |
+
|
| 1262 |
+
for (const withQ of [false, true]) {
|
| 1263 |
+
armed();
|
| 1264 |
+
const extra = withQ ? { sheaf: true } : {};
|
| 1265 |
+
const press = () => { drive(0.2, { ...extra, pickup: true }); drive(0.15, extra); };
|
| 1266 |
+
// THE BAKEHOUSE, AND THE TWO FRAGMENTS ARE THE FORGE'S AND THE WELL
|
| 1267 |
+
// HOUSE'S. Both are wrong here, on purpose.
|
| 1268 |
+
//
|
| 1269 |
+
// The first pass at this used fragment 1 as "a different fragment" at
|
| 1270 |
+
// house 1 — which is the bakehouse's OWN fragment, so it was correct,
|
| 1271 |
+
// its owner walked home, the patch settled, and every row after it
|
| 1272 |
+
// measured the lock refusing rather than the thing it meant to measure.
|
| 1273 |
+
// The check was wrong and the game was right.
|
| 1274 |
+
const H = 1;
|
| 1275 |
+
const A = 0, B = 2;
|
| 1276 |
+
const h = s.houses[H];
|
| 1277 |
+
|
| 1278 |
+
// 1. A letter on the nail, anything on top -> takes the letter.
|
| 1279 |
+
put(0); stand(H, withQ);
|
| 1280 |
+
// the door has to have been looked at at least once for anything to be
|
| 1281 |
+
// offered, which is the latch, not a mode
|
| 1282 |
+
drive(1.4, { torch: true });
|
| 1283 |
+
stand(H, withQ);
|
| 1284 |
+
press();
|
| 1285 |
+
const tookLetter = h.letterTaken && h.filed == null;
|
| 1286 |
+
|
| 1287 |
+
// 2. A bare patch, a fragment on top -> pins it.
|
| 1288 |
+
put(A); stand(H, withQ); press();
|
| 1289 |
+
const pinned = h.filed === A;
|
| 1290 |
+
|
| 1291 |
+
// 3. A fragment in the patch, a DIFFERENT fragment on top -> swap.
|
| 1292 |
+
put(B); stand(H, withQ); press();
|
| 1293 |
+
const swapped = h.filed === B;
|
| 1294 |
+
|
| 1295 |
+
// 4. A fragment in the patch, the SAME one on top -> takes it back.
|
| 1296 |
+
put(B); stand(H, withQ); press();
|
| 1297 |
+
const tookBackSame = h.filed == null;
|
| 1298 |
+
|
| 1299 |
+
// 5. A fragment in the patch, NOT a fragment on top -> takes it back.
|
| 1300 |
+
put(A); stand(H, withQ); press(); // pin one again
|
| 1301 |
+
put(null); stand(H, withQ); press(); // a letter on top
|
| 1302 |
+
const tookBackWithLetter = h.filed == null;
|
| 1303 |
+
|
| 1304 |
+
// 6. A bare patch, not a fragment on top -> nothing.
|
| 1305 |
+
put(null); stand(H, withQ);
|
| 1306 |
+
const offeredWithLetter = s.canMark;
|
| 1307 |
+
press();
|
| 1308 |
+
const bareAndLetterDoesNothing = h.filed == null && !offeredWithLetter;
|
| 1309 |
+
|
| 1310 |
+
// 7. A settled patch -> nothing. The lock stands.
|
| 1311 |
+
const F = 0;
|
| 1312 |
+
const forge = s.houses[F];
|
| 1313 |
+
put(0); stand(F, withQ);
|
| 1314 |
+
drive(1.4, { torch: true });
|
| 1315 |
+
stand(F, withQ); press(); // letter off
|
| 1316 |
+
put(0); stand(F, withQ); press(); // the forge's own, so it settles
|
| 1317 |
+
const settled = forge.settled && forge.filed === 0;
|
| 1318 |
+
put(B); stand(F, withQ); press(); // try to swap it
|
| 1319 |
+
const lockHeld = forge.filed === 0 && settled;
|
| 1320 |
+
|
| 1321 |
+
rows.push({ withQ, tookLetter, pinned, swapped, tookBackSame,
|
| 1322 |
+
tookBackWithLetter, bareAndLetterDoesNothing, lockHeld });
|
| 1323 |
+
}
|
| 1324 |
+
scene.reset();
|
| 1325 |
+
const ok = rows.every((r) => Object.entries(r)
|
| 1326 |
+
.every(([k, v]) => k === 'withQ' || v === true));
|
| 1327 |
+
// AND Q CHANGES NOTHING. Every row, both ways, identical.
|
| 1328 |
+
const sameEitherWay = JSON.stringify({ ...rows[0], withQ: 0 }) ===
|
| 1329 |
+
JSON.stringify({ ...rows[1], withQ: 0 });
|
| 1330 |
+
|
| 1331 |
+
return {
|
| 1332 |
+
name: 'E at the door',
|
| 1333 |
+
pass: ok && sameEitherWay,
|
| 1334 |
+
everyRowHolds: ok,
|
| 1335 |
+
andTheSheafMakesNoDifference: sameEitherWay,
|
| 1336 |
+
rows,
|
| 1337 |
+
};
|
| 1338 |
+
}
|
| 1339 |
+
|
| 1340 |
function walksHome() {
|
| 1341 |
const carriers = armed();
|
| 1342 |
const owner = carriers.find((d) => d.kept === 0); // the forge's
|
|
|
|
| 1767 |
mark();
|
| 1768 |
const needsADoor = filed().join(',') === before;
|
| 1769 |
|
| 1770 |
+
// 6. AND IT DOES NOT NEED THE SHEAF UP. THIS CLAIM IS INVERTED.
|
| 1771 |
+
//
|
| 1772 |
+
// It used to assert the opposite — that pinning with the sheaf down did
|
| 1773 |
+
// nothing — and that rule was played and it did not survive contact.
|
| 1774 |
+
// Three conditions and a mode: hold Q, page to the fragment, press E at
|
| 1775 |
+
// a door. Nobody does that with paper.
|
| 1776 |
+
//
|
| 1777 |
+
// The top page of the pile is what he is holding, and E acts on what he
|
| 1778 |
+
// is holding. So the same press with the sheaf DOWN has to do the same
|
| 1779 |
+
// thing the press with it up did, and this asserts exactly that.
|
| 1780 |
+
s.prince.x = s.prince.prevX = s.houses[0].x - 1500;
|
| 1781 |
+
s.sheaf.index = fragA;
|
| 1782 |
+
drive(0.4); // sheaf down, walking about
|
| 1783 |
+
const onTopWithItDown = s.page()?.kept === 2 && !s.sheaf.up;
|
| 1784 |
+
s.prince.x = s.prince.prevX = s.houses[0].x - 300;
|
| 1785 |
+
s.prince.facing = 1;
|
| 1786 |
+
s.camera.reset(s.prince.x);
|
| 1787 |
+
drive(0.5); // NO sheaf, NO torch
|
| 1788 |
+
const offeredWithItDown = s.canPin;
|
| 1789 |
+
tap(); // plain E
|
| 1790 |
+
const pinnedWithItDown = s.houses[0].filed === 2;
|
| 1791 |
+
// And taking it back down is the same press again.
|
| 1792 |
+
tap();
|
| 1793 |
+
const takenBackWithItDown = s.houses[0].filed == null;
|
| 1794 |
+
const noSheafNeeded = onTopWithItDown && offeredWithItDown &&
|
| 1795 |
+
pinnedWithItDown && takenBackWithItDown;
|
| 1796 |
+
// Put the world back where claim 7 expects it.
|
| 1797 |
+
const before2 = filed().join(',');
|
| 1798 |
// A LETTER IS NOT A CLAIM. Only a torn page off a body can be filed — the
|
| 1799 |
// letter is what the house already says about itself.
|
| 1800 |
s.sheaf.index = s.sheaf.pages.findIndex((p) => p.kind === 'letter');
|
| 1801 |
at(0); mark();
|
| 1802 |
+
const mapCannotBeFiled = s.sheaf.index < 0 || filed().join(',') === before2;
|
| 1803 |
|
| 1804 |
// 7. NO FEEDBACK, EVER. Nothing in the state says right or wrong, and an
|
| 1805 |
// empty plot is not flagged. Four filed and one empty is the finding,
|
|
|
|
| 1817 |
|
| 1818 |
return {
|
| 1819 |
name: 'marking is a claim',
|
| 1820 |
+
pass: nothingAutoFiles && needsTheLetterOff && noSheafNeeded &&
|
| 1821 |
afterFirst[0] === 2 && afterReplace[0] === 3 &&
|
| 1822 |
afterMove[0] == null && afterMove[1] === 3 && onlyOnce &&
|
| 1823 |
+
needsADoor && mapCannotBeFiled && noVerdict &&
|
| 1824 |
frozenAtTheGate,
|
| 1825 |
nothingFilesItself: nothingAutoFiles,
|
| 1826 |
andThereIsNoSlotUntilTheLetterIsOff: needsTheLetterOff,
|
| 1827 |
theSamePressTakesTheLetterInstead: tookTheLetterInstead,
|
| 1828 |
afterFirst, afterReplace, afterMove,
|
| 1829 |
oneFragmentOneHouse: onlyOnce,
|
| 1830 |
+
needsALitDoor: needsADoor,
|
| 1831 |
+
andDoesNotNeedTheSheafUp: noSheafNeeded,
|
| 1832 |
+
withTheSheafDown: { onTop: onTopWithItDown, offered: offeredWithItDown,
|
| 1833 |
+
pinned: pinnedWithItDown, takenBack: takenBackWithItDown },
|
| 1834 |
aLetterCannotBeFiled: mapCannotBeFiled,
|
| 1835 |
noRightOrWrongAnywhere: noVerdict,
|
| 1836 |
frozenAtTheGate,
|
|
|
|
| 1997 |
return frame();
|
| 1998 |
});
|
| 1999 |
|
| 2000 |
+
// 4. AND ON TOUCH THERE IS ONE CONTEXTUAL BUTTON, NOT TWO.
|
| 2001 |
+
//
|
| 2002 |
+
// MARK lived on the right wheel because pinning a page needed PAGES
|
| 2003 |
+
// held and TAKE pressed and one thumb cannot do both. The top page is
|
| 2004 |
+
// now what he is holding and TAKE acts on it, so the second button is
|
| 2005 |
+
// deleted. Asserted rather than trusted, because a stray button in the
|
| 2006 |
+
// markup is invisible on a desktop and is the whole control scheme on
|
| 2007 |
+
// a phone.
|
| 2008 |
+
const pad = document.getElementById('pad');
|
| 2009 |
+
const acts = pad
|
| 2010 |
+
? [...pad.querySelectorAll('button[data-act]')].map((b) => b.dataset.act)
|
| 2011 |
+
: [];
|
| 2012 |
+
const touch = {
|
| 2013 |
+
noMarkButton: !document.getElementById('markbtn'),
|
| 2014 |
+
// Exactly one thing on the pad fires the contextual verb.
|
| 2015 |
+
onePickupButton: acts.filter((a) => a === 'pickup').length === 1,
|
| 2016 |
+
// And nothing is labelled with the verb that no longer exists.
|
| 2017 |
+
nothingSaysMark: !pad || !/\bMARK\b/.test(pad.textContent),
|
| 2018 |
+
acts,
|
| 2019 |
+
};
|
| 2020 |
+
|
| 2021 |
return {
|
| 2022 |
name: 'the keys are shown',
|
| 2023 |
pass: withoutTheReadout > 1500 && withTheReadout > 1500 &&
|
| 2024 |
+
whileReading < withoutTheReadout * 0.25 &&
|
| 2025 |
+
touch.noMarkButton && touch.onePickupButton && touch.nothingSaysMark,
|
| 2026 |
inkWithTheReadoutOff: withoutTheReadout,
|
| 2027 |
inkWithItOn: withTheReadout,
|
| 2028 |
inkWhileReadingALetter: whileReading,
|
| 2029 |
survivesDebugBeingOff: withoutTheReadout > 1500,
|
| 2030 |
getsOutOfTheWayOfAPage: whileReading < withoutTheReadout * 0.25,
|
| 2031 |
+
andOneContextualButtonOnTouch: touch,
|
| 2032 |
};
|
| 2033 |
}
|
| 2034 |
|
|
|
|
| 2231 |
doorsRead(), pagesCost(), sheafPages(), markingIsAClaim(),
|
| 2232 |
theRoomAsks(), aGlanceIsFree(), theLightGoesOut(), theKeysAreShown(), bandFits(),
|
| 2233 |
dealObeysRules(), swingerIsBait(),
|
| 2234 |
+
topPageIsTheHand(), eAtTheDoor(),
|
| 2235 |
walksHome(), keyBehindTheDoor(), residueStaysLocked(),
|
| 2236 |
nothingOutOfReach(), healthCosts(), theGate()];
|
| 2237 |
return { pass: out.every((r) => r.pass), results: out };
|
|
|
|
| 2240 |
return { figureContrast, watcherContrast, brightMass, skyWeave,
|
| 2241 |
bowlLayout, bodiesPersist, wholeLevelWalkable, stillUntilLit, doorsRead,
|
| 2242 |
pagesCost, sheafPages, markingIsAClaim, theRoomAsks, aGlanceIsFree, theLightGoesOut, theKeysAreShown, bandFits, dealObeysRules, swingerIsBait,
|
| 2243 |
+
topPageIsTheHand, eAtTheDoor,
|
| 2244 |
walksHome, keyBehindTheDoor, residueStaysLocked,
|
| 2245 |
nothingOutOfReach, healthCosts, theGate,
|
| 2246 |
all, frameAt, frameAtX };
|
src/input.js
CHANGED
|
@@ -158,31 +158,24 @@ export function createInput(target, { onReset } = {}) {
|
|
| 158 |
}
|
| 159 |
|
| 160 |
/**
|
| 161 |
-
* Lights TAKE only when there is something to do with it
|
| 162 |
*
|
| 163 |
-
*
|
| 164 |
-
*
|
| 165 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
*/
|
| 167 |
const takeBtn = pad?.querySelector('.wheel [data-act="pickup"]');
|
| 168 |
-
// The second one, over by the walk arrows. Filing needs PAGES held and TAKE
|
| 169 |
-
// pressed and both of those are on the left wheel, which is one thumb's worth
|
| 170 |
-
// of buttons asking for two thumbs. This is the one the right thumb can reach
|
| 171 |
-
// without letting go of anything.
|
| 172 |
-
const markBtn = pad?.querySelector('#markbtn');
|
| 173 |
let takeLive = false;
|
| 174 |
-
const setTakeAvailable = (yes
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
// ONE OF THEM AT A TIME. The wheel's button never changes what it says —
|
| 179 |
-
// it is TAKE, and it goes dark while there is a MARK offered on the other
|
| 180 |
-
// side. A dim MARK here and a bright MARK there is two buttons asking to
|
| 181 |
-
// be pressed for one act.
|
| 182 |
-
const wheelLive = yes && !marking;
|
| 183 |
-
if (wheelLive === takeLive) return;
|
| 184 |
-
takeLive = wheelLive;
|
| 185 |
-
takeBtn.classList.toggle('live', wheelLive);
|
| 186 |
};
|
| 187 |
|
| 188 |
target.addEventListener('pointerdown', onPointerDown);
|
|
|
|
| 158 |
}
|
| 159 |
|
| 160 |
/**
|
| 161 |
+
* Lights TAKE only when there is something to do with it.
|
| 162 |
*
|
| 163 |
+
* ONE BUTTON, ONE SLOT, NO LABEL CHANGE. It used to be two: TAKE on the left
|
| 164 |
+
* wheel and a second MARK button over by the walk arrows, because pinning a
|
| 165 |
+
* page needed PAGES held and TAKE pressed and one thumb cannot do both.
|
| 166 |
+
*
|
| 167 |
+
* The top page of the pile is now what he is holding, so TAKE acts on it
|
| 168 |
+
* wherever he stands and the second button is deleted. Whether this press
|
| 169 |
+
* takes a letter, pins a page, takes one back down, takes a key or picks
|
| 170 |
+
* something off the ground is decided by where he is standing — which is
|
| 171 |
+
* what TAKE has meant everywhere else in this game since the first object.
|
| 172 |
*/
|
| 173 |
const takeBtn = pad?.querySelector('.wheel [data-act="pickup"]');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
let takeLive = false;
|
| 175 |
+
const setTakeAvailable = (yes) => {
|
| 176 |
+
if (!takeBtn || yes === takeLive) return;
|
| 177 |
+
takeLive = yes;
|
| 178 |
+
takeBtn.classList.toggle('live', yes);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
};
|
| 180 |
|
| 181 |
target.addEventListener('pointerdown', onPointerDown);
|
src/main.js
CHANGED
|
@@ -109,7 +109,7 @@ const loop = createLoop({
|
|
| 109 |
// decides. This used to re-run the ground scan itself, which was two
|
| 110 |
// places that had to agree about what is within reach.
|
| 111 |
input.setTakeAvailable?.(st.canMark || st.canTakeLetter || st.canTakeKey ||
|
| 112 |
-
st.canPlaceKey || st.canTake
|
| 113 |
}
|
| 114 |
},
|
| 115 |
render: (alpha) => {
|
|
|
|
| 109 |
// decides. This used to re-run the ground scan itself, which was two
|
| 110 |
// places that had to agree about what is within reach.
|
| 111 |
input.setTakeAvailable?.(st.canMark || st.canTakeLetter || st.canTakeKey ||
|
| 112 |
+
st.canPlaceKey || st.canTake);
|
| 113 |
}
|
| 114 |
},
|
| 115 |
render: (alpha) => {
|
src/render.js
CHANGED
|
@@ -365,6 +365,68 @@ export function createRenderer(ctx) {
|
|
| 365 |
* No error state, no completion state, and an empty slot is never flagged.
|
| 366 |
* The empty slot is the finding.
|
| 367 |
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
function drawSheet(c, s, mode, px, camX, face) {
|
| 369 |
const sheet = s.sheetClip;
|
| 370 |
const pose = s.prince.clip;
|
|
@@ -993,6 +1055,7 @@ export function createRenderer(ctx) {
|
|
| 993 |
drawKept(ctx, s.kept, d, 'dark', dx2, camX, s.clock);
|
| 994 |
}
|
| 995 |
drawClip(ctx, s.prince.clip, s.prince.frame, 'dark', px, GROUND_Y + limpDip, camX, face, s.prince.eyeDim);
|
|
|
|
| 996 |
drawSheet(ctx, s, 'dark', px, camX, face);
|
| 997 |
if (torchUp) drawClip(ctx, s.flame.clip, s.flame.frame, 'dark', handX, handY, camX, face);
|
| 998 |
stats.bgDrawn += s.background.drawNear(ctx, camX, W, H, 'dark', px);
|
|
@@ -1040,6 +1103,7 @@ export function createRenderer(ctx) {
|
|
| 1040 |
drawKept(octx, s.kept, d, 'lit', dx2, camX, s.clock);
|
| 1041 |
}
|
| 1042 |
drawClip(octx, s.prince.clip, s.prince.frame, 'dark', px, GROUND_Y + limpDip, camX, face, s.prince.eyeDim);
|
|
|
|
| 1043 |
// He is never lit by his own torch. The SHEET is — it is the one thing
|
| 1044 |
// he carries that the light is for.
|
| 1045 |
drawSheet(octx, s, 'lit', px, camX, face);
|
|
@@ -1178,11 +1242,12 @@ export function createRenderer(ctx) {
|
|
| 1178 |
(s.prince.inventory.length
|
| 1179 |
? s.prince.inventory.map((it) => (it.seen ? it.answers : '?')).join(', ')
|
| 1180 |
: '—') +
|
| 1181 |
-
(s.
|
| 1182 |
-
: s.
|
| 1183 |
-
: s.
|
| 1184 |
-
: s.
|
| 1185 |
-
:
|
|
|
|
| 1186 |
// ONLY WHEN IT IS NOT WORKING. Spelled out because a phone has no
|
| 1187 |
// console and no backtick key — "running / playback" and still silent
|
| 1188 |
// on an iPhone means the ringer switch, not the game. Once it is
|
|
|
|
| 365 |
* No error state, no completion state, and an empty slot is never flagged.
|
| 366 |
* The empty slot is the finding.
|
| 367 |
*/
|
| 368 |
+
/**
|
| 369 |
+
* THE PAGE IN HIS HAND, and it is the whole affordance.
|
| 370 |
+
*
|
| 371 |
+
* The top of the pile is what he is holding, and E at a door acts on it. A
|
| 372 |
+
* player who cannot see what they are holding has to raise the sheaf to find
|
| 373 |
+
* out, which is the mode this change exists to delete — so he holds it where
|
| 374 |
+
* you can see it, and the SHAPE says what E will do:
|
| 375 |
+
*
|
| 376 |
+
* a torn page a fragment. E at a bare patch pins it
|
| 377 |
+
* a whole page a letter. E at a patch takes back whatever is in it
|
| 378 |
+
* nothing nothing to pin
|
| 379 |
+
*
|
| 380 |
+
* BOTH PASSES, AND ALWAYS THE DARK ART. He is drawn as a silhouette even
|
| 381 |
+
* inside his own torchlight — a man cannot light himself — and a page in his
|
| 382 |
+
* hand is part of him. It reads for the same reason a carrier's fragment
|
| 383 |
+
* reads: the hand is at the edge of his outline, so the page breaks it.
|
| 384 |
+
*
|
| 385 |
+
* IT RIDES mapPivot, which is already measured on her standing drawing and
|
| 386 |
+
* is already the hand the sheaf is held in. Nothing new to place: when the
|
| 387 |
+
* sheaf goes up, the sheet covers this exact spot, which is why this hides.
|
| 388 |
+
*
|
| 389 |
+
* AND ONLY ON THE STANDING POSE. Her run frames carry no hand anchor, so he
|
| 390 |
+
* is empty-handed while moving and holding it the moment he stops — which is
|
| 391 |
+
* the moment it matters and the only moment E does anything.
|
| 392 |
+
*/
|
| 393 |
+
function drawHeldPage(c, s, px, camX, face) {
|
| 394 |
+
if (s.sheaf?.up) return;
|
| 395 |
+
const pg = s.handPage;
|
| 396 |
+
if (!pg) return;
|
| 397 |
+
const pose = s.prince.clip;
|
| 398 |
+
if (!pose?.mapPivot) return;
|
| 399 |
+
const art = pg.kind === 'fragment' ? s.kept : pg.kind === 'letter' ? s.letterArt : null;
|
| 400 |
+
if (!art) return;
|
| 401 |
+
const idx = pg.kind === 'fragment' ? pg.kept : pg.house;
|
| 402 |
+
const frame = art.frames[idx];
|
| 403 |
+
if (!frame) return;
|
| 404 |
+
|
| 405 |
+
const ink = art.ink ?? { x: 0, y: 0, w: art.width, h: art.height };
|
| 406 |
+
// ONE SCALE FOR BOTH, AND THAT IS THE WHOLE DIFFERENCE BETWEEN THEM.
|
| 407 |
+
//
|
| 408 |
+
// Scaling each to the same height came out at 827 pixels of page for a
|
| 409 |
+
// fragment and 756 for a letter, differing by 139 — the tear, and nothing
|
| 410 |
+
// else. Which is measuring a 2.6px scribble at arm's length.
|
| 411 |
+
//
|
| 412 |
+
// The art already knows: the whole page's ink is 54 tall and a piece torn
|
| 413 |
+
// off it is 46. Fixing the scale to the page's own units keeps that, so a
|
| 414 |
+
// fragment is visibly the smaller thing. Divided by the LETTER's ink, so
|
| 415 |
+
// the full page is the one that comes out at the size that was chosen.
|
| 416 |
+
//
|
| 417 |
+
// Small either way. It is a page held down by the side, not one being read.
|
| 418 |
+
const k = 40 / ((s.letterArt?.ink?.h) || ink.h);
|
| 419 |
+
const w = ink.w * k;
|
| 420 |
+
const h = ink.h * k;
|
| 421 |
+
const ox = (pose.mapPivot[0] - pose.pivotX) * face;
|
| 422 |
+
const oy = pose.mapPivot[1] - pose.pivotY;
|
| 423 |
+
const sx = Math.round(px - camX + ox);
|
| 424 |
+
const sy = Math.round(GROUND_Y + oy);
|
| 425 |
+
const left = face >= 0 ? sx - w * 0.2 : sx - w * 0.8;
|
| 426 |
+
c.drawImage(frame.dark, left - ink.x * k, sy - h * 0.5 - ink.y * k,
|
| 427 |
+
art.width * k, art.height * k);
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
function drawSheet(c, s, mode, px, camX, face) {
|
| 431 |
const sheet = s.sheetClip;
|
| 432 |
const pose = s.prince.clip;
|
|
|
|
| 1055 |
drawKept(ctx, s.kept, d, 'dark', dx2, camX, s.clock);
|
| 1056 |
}
|
| 1057 |
drawClip(ctx, s.prince.clip, s.prince.frame, 'dark', px, GROUND_Y + limpDip, camX, face, s.prince.eyeDim);
|
| 1058 |
+
drawHeldPage(ctx, s, px, camX, face);
|
| 1059 |
drawSheet(ctx, s, 'dark', px, camX, face);
|
| 1060 |
if (torchUp) drawClip(ctx, s.flame.clip, s.flame.frame, 'dark', handX, handY, camX, face);
|
| 1061 |
stats.bgDrawn += s.background.drawNear(ctx, camX, W, H, 'dark', px);
|
|
|
|
| 1103 |
drawKept(octx, s.kept, d, 'lit', dx2, camX, s.clock);
|
| 1104 |
}
|
| 1105 |
drawClip(octx, s.prince.clip, s.prince.frame, 'dark', px, GROUND_Y + limpDip, camX, face, s.prince.eyeDim);
|
| 1106 |
+
drawHeldPage(octx, s, px, camX, face);
|
| 1107 |
// He is never lit by his own torch. The SHEET is — it is the one thing
|
| 1108 |
// he carries that the light is for.
|
| 1109 |
drawSheet(octx, s, 'lit', px, camX, face);
|
|
|
|
| 1242 |
(s.prince.inventory.length
|
| 1243 |
? s.prince.inventory.map((it) => (it.seen ? it.answers : '?')).join(', ')
|
| 1244 |
: '—') +
|
| 1245 |
+
(s.canPin ? ' [E] PIN IT HERE'
|
| 1246 |
+
: s.canMark ? ' [E] take it back down'
|
| 1247 |
+
: s.canTakeLetter ? ' [E] take the letter'
|
| 1248 |
+
: s.canPlaceKey ? ` [E] put a key in (${s.prince.keys} in hand)`
|
| 1249 |
+
: s.canTakeKey ? ' [E] take the key'
|
| 1250 |
+
: nearKept ? ' [E] take' : '') +
|
| 1251 |
// ONLY WHEN IT IS NOT WORKING. Spelled out because a phone has no
|
| 1252 |
// console and no backtick key — "running / playback" and still silent
|
| 1253 |
// on an iPhone means the ringer switch, not the game. Once it is
|
src/scene_level.js
CHANGED
|
@@ -1049,12 +1049,15 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 1049 |
// Lighting one is the only thing that says who lived here. It latches, the
|
| 1050 |
// same way seeing an object on the ground latches, and for the same reason:
|
| 1051 |
// he looked, and looking is not something he can un-do.
|
| 1052 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1053 |
let litLetter = null;
|
| 1054 |
for (const h of HOUSES) {
|
| 1055 |
const [dx, dy] = doorPoint(h);
|
| 1056 |
if (!torch.covers(dx, dy)) continue;
|
| 1057 |
-
lit = h;
|
| 1058 |
h.seen = true;
|
| 1059 |
// And the letter on its wall. Same latch as an object on the ground: he
|
| 1060 |
// looked, and looking is not something he can un-do.
|
|
@@ -1163,7 +1166,38 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 1163 |
const wantsPickup = pickupHeld && !prevPickup;
|
| 1164 |
prevPickup = pickupHeld;
|
| 1165 |
input.pickup = false;
|
| 1166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1167 |
// AND NOT BEFORE THE LETTER IS OFF THE NAIL.
|
| 1168 |
//
|
| 1169 |
// There is one spot beside each door and the letter is on it. Take the
|
|
@@ -1179,40 +1213,60 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 1179 |
//
|
| 1180 |
// AND NOT AT A DOOR THAT IS FINISHED WITH. Someone walked home to it; the
|
| 1181 |
// patch has the only thing it was ever going to hold.
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1185 |
// WHICH house it would go under. The sheet draws that house's plot beside
|
| 1186 |
-
// the page when this is set, so the paper itself says "this one, here"
|
| 1187 |
-
|
| 1188 |
-
state.
|
| 1189 |
-
state.canTakeLetter = !canMark && !!litLetter;
|
| 1190 |
-
// LETTER FIRST, THEN KEY. At an open door with the letter still on the
|
| 1191 |
-
// wall both are offered by the same button, so they queue: press once for
|
| 1192 |
-
// the reading, again for the key. Evidence before the thing that opens a
|
| 1193 |
-
// gate, which is the order this whole game is arguing for.
|
| 1194 |
-
state.canTakeKey = !canMark && !litLetter && !!litKey;
|
| 1195 |
// AND PUTTING ONE IN. He stops 380px short of the leaf and can never get
|
| 1196 |
// closer, so the reach is the door's, not the ground's.
|
| 1197 |
state.canPlaceKey = !canMark && prince.keys > 0 && !gate.through &&
|
| 1198 |
gate.placed < TUNING.GATE_WANTS &&
|
| 1199 |
Math.abs(prince.x - GATE_X) <= TUNING.DOOR_REACH;
|
| 1200 |
-
if (wantsPickup && canMark) {
|
| 1201 |
-
if (
|
| 1202 |
-
// TAKE IT BACK
|
| 1203 |
-
//
|
| 1204 |
-
//
|
| 1205 |
-
//
|
| 1206 |
-
|
|
|
|
|
|
|
| 1207 |
audio?.mark?.();
|
| 1208 |
} else {
|
| 1209 |
-
// MOVE, THEN REPLACE.
|
| 1210 |
-
// wherever it was, and
|
| 1211 |
-
// out. Two fragments can never sit
|
| 1212 |
-
// never sit
|
| 1213 |
// putting a piece of paper down on a table already does.
|
| 1214 |
-
for (const h of HOUSES) if (h.filed ===
|
| 1215 |
-
|
| 1216 |
audio?.mark?.();
|
| 1217 |
|
| 1218 |
// AND IF IT WAS THEIRS, THEY GO HOME.
|
|
@@ -1222,11 +1276,11 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 1222 |
// there is nowhere for it to drift to.
|
| 1223 |
//
|
| 1224 |
// NOTHING IS SAID. No tick, no sound of being right, no change to the
|
| 1225 |
-
//
|
| 1226 |
// to find out is that it walks up the street.
|
| 1227 |
-
if (
|
| 1228 |
-
const owner = drained.find((d) => d.kept ===
|
| 1229 |
-
if (owner) {
|
| 1230 |
}
|
| 1231 |
}
|
| 1232 |
}
|
|
@@ -1252,7 +1306,7 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 1252 |
// fragment, and this needs a letter still lying in a lit room. Ahead of the
|
| 1253 |
// object at his feet because he is looking into the room, and because the
|
| 1254 |
// clear pocket round every door keeps drops out of this spot anyway.
|
| 1255 |
-
if (wantsPickup &&
|
| 1256 |
litLetter.letterTaken = true;
|
| 1257 |
filePage({ kind: 'letter', house: litLetter.index });
|
| 1258 |
audio?.mark?.();
|
|
@@ -1270,7 +1324,7 @@ export async function createLevelScene(clips, { audio = null, seed = null, lette
|
|
| 1270 |
litKey.keyTaken = true;
|
| 1271 |
prince.keys++;
|
| 1272 |
audio?.mark?.();
|
| 1273 |
-
} else if (wantsPickup && !canMark && nearest && !prince.attacking) {
|
| 1274 |
nearest.keptTaken = true;
|
| 1275 |
run.taken++;
|
| 1276 |
prince.inventory.push({
|
|
|
|
| 1049 |
// Lighting one is the only thing that says who lived here. It latches, the
|
| 1050 |
// same way seeing an object on the ground latches, and for the same reason:
|
| 1051 |
// he looked, and looking is not something he can un-do.
|
| 1052 |
+
// IT LATCHES AND NOTHING READS IT THIS FRAME ANY MORE. There used to be a
|
| 1053 |
+
// `lit` house here, meaning "the torch is on this door right now", and it
|
| 1054 |
+
// was what marking tested. Marking now tests a LATCH plus a distance, the
|
| 1055 |
+
// same deal the letter and the key already had — so all this loop does is
|
| 1056 |
+
// remember that he looked.
|
| 1057 |
let litLetter = null;
|
| 1058 |
for (const h of HOUSES) {
|
| 1059 |
const [dx, dy] = doorPoint(h);
|
| 1060 |
if (!torch.covers(dx, dy)) continue;
|
|
|
|
| 1061 |
h.seen = true;
|
| 1062 |
// And the letter on its wall. Same latch as an object on the ground: he
|
| 1063 |
// looked, and looking is not something he can un-do.
|
|
|
|
| 1166 |
const wantsPickup = pickupHeld && !prevPickup;
|
| 1167 |
prevPickup = pickupHeld;
|
| 1168 |
input.pickup = false;
|
| 1169 |
+
// --- what is in his hand ------------------------------------------------
|
| 1170 |
+
//
|
| 1171 |
+
// THE TOP PAGE OF THE PILE IS WHAT HE IS HOLDING, and the pile does not
|
| 1172 |
+
// have to be up for him to be holding it.
|
| 1173 |
+
//
|
| 1174 |
+
// It used to. Pinning a fragment meant holding Q, paging to it, and
|
| 1175 |
+
// pressing E at a door — three conditions and a mode, and it was played and
|
| 1176 |
+
// nobody did it. Nobody does that with paper either: you take the page you
|
| 1177 |
+
// want, put the rest away, walk over, and pin it.
|
| 1178 |
+
//
|
| 1179 |
+
// So the sheaf is for CHOOSING and E is for DOING, and E never asks about
|
| 1180 |
+
// the sheaf. Q still raises the torch, still stops him, still shows the
|
| 1181 |
+
// band and pages with the arrows. It has just stopped being a prerequisite.
|
| 1182 |
+
const handPage = page();
|
| 1183 |
+
state.handPage = handPage;
|
| 1184 |
+
|
| 1185 |
+
// THE DOOR HE IS AT, AND IT IS LATCHED RATHER THAN LIT.
|
| 1186 |
+
//
|
| 1187 |
+
// `lit` above is whether the torch is ON a door this frame, and that was
|
| 1188 |
+
// the marking test — which worked only because holding the sheaf forced the
|
| 1189 |
+
// torch up. With the sheaf out of it, the torch is down and `lit` is null,
|
| 1190 |
+
// so the old test could never fire again.
|
| 1191 |
+
//
|
| 1192 |
+
// The letter and the key already solved this: seeing latches, and taking is
|
| 1193 |
+
// a separate act that only needs him to still be standing there. The patch
|
| 1194 |
+
// is the third thing on that wall and it gets the same deal.
|
| 1195 |
+
let nearDoor = null;
|
| 1196 |
+
for (const h of HOUSES) {
|
| 1197 |
+
if (!h.seen) continue;
|
| 1198 |
+
const p = letterPoint(h);
|
| 1199 |
+
if (p && Math.abs(prince.x - p[0]) <= TUNING.DOOR_REACH) { nearDoor = h; break; }
|
| 1200 |
+
}
|
| 1201 |
// AND NOT BEFORE THE LETTER IS OFF THE NAIL.
|
| 1202 |
//
|
| 1203 |
// There is one spot beside each door and the letter is on it. Take the
|
|
|
|
| 1213 |
//
|
| 1214 |
// AND NOT AT A DOOR THAT IS FINISHED WITH. Someone walked home to it; the
|
| 1215 |
// patch has the only thing it was ever going to hold.
|
| 1216 |
+
//
|
| 1217 |
+
// AND NOT AT A DOOR THAT IS FINISHED WITH. Someone walked home to it; the
|
| 1218 |
+
// patch has the only thing it was ever going to hold.
|
| 1219 |
+
const patchTakes = !!nearDoor && !gate.through && !nearDoor.settled &&
|
| 1220 |
+
nearDoor.letterTaken;
|
| 1221 |
+
const holding = handPage?.kind === 'fragment' ? handPage.kept : null;
|
| 1222 |
+
|
| 1223 |
+
// PIN, or TAKE BACK, and which one is decided by what is already there.
|
| 1224 |
+
//
|
| 1225 |
+
// THE SAME PAGE THAT IS ALREADY PINNED TAKES IT BACK. The table this comes
|
| 1226 |
+
// from does not have a row for it — it has "a different fragment evicts and
|
| 1227 |
+
// pins" and "not a fragment takes it back" — and holding the page that is
|
| 1228 |
+
// already on the wall is neither. Evicting it and pinning it again is a
|
| 1229 |
+
// press that does nothing, so it is the other one: he is holding the thing
|
| 1230 |
+
// in front of him, and E takes it down.
|
| 1231 |
+
const canRetrieve = patchTakes && nearDoor.filed != null &&
|
| 1232 |
+
(holding === null || holding === nearDoor.filed);
|
| 1233 |
+
const canPin = patchTakes && holding !== null && holding !== nearDoor.filed;
|
| 1234 |
+
const canMark = canPin || canRetrieve;
|
| 1235 |
+
|
| 1236 |
+
// LETTER FIRST, THEN THE PATCH, THEN THE KEY. An unread door hands over the
|
| 1237 |
+
// reading before it will accept a claim about it, and the thing that opens
|
| 1238 |
+
// a gate comes last. Evidence before conclusions before rewards, which is
|
| 1239 |
+
// the order this whole game is arguing for.
|
| 1240 |
+
state.canTakeLetter = !!litLetter;
|
| 1241 |
+
state.canMark = !litLetter && canMark;
|
| 1242 |
+
state.canPin = !litLetter && canPin;
|
| 1243 |
// WHICH house it would go under. The sheet draws that house's plot beside
|
| 1244 |
+
// the page when this is set, so the paper itself says "this one, here".
|
| 1245 |
+
state.markHouse = state.canPin ? nearDoor.index : null;
|
| 1246 |
+
state.canTakeKey = !litLetter && !canMark && !!litKey;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1247 |
// AND PUTTING ONE IN. He stops 380px short of the leaf and can never get
|
| 1248 |
// closer, so the reach is the door's, not the ground's.
|
| 1249 |
state.canPlaceKey = !canMark && prince.keys > 0 && !gate.through &&
|
| 1250 |
gate.placed < TUNING.GATE_WANTS &&
|
| 1251 |
Math.abs(prince.x - GATE_X) <= TUNING.DOOR_REACH;
|
| 1252 |
+
if (wantsPickup && state.canMark && !prince.attacking) {
|
| 1253 |
+
if (canRetrieve) {
|
| 1254 |
+
// TAKE IT BACK DOWN. Either he is holding the page that is already
|
| 1255 |
+
// pinned here, or he is holding nothing that could go in — and in both
|
| 1256 |
+
// cases the only thing on that wall he can act on is what is on it.
|
| 1257 |
+
//
|
| 1258 |
+
// It never left the sheaf. The patch holds a CLAIM, not the paper, so
|
| 1259 |
+
// there is nothing to hand back.
|
| 1260 |
+
nearDoor.filed = null;
|
| 1261 |
audio?.mark?.();
|
| 1262 |
} else {
|
| 1263 |
+
// MOVE, THEN REPLACE. Pinning a fragment somewhere else takes it off
|
| 1264 |
+
// wherever it was, and pinning a different one here pushes the old one
|
| 1265 |
+
// out. Two fragments can never sit in one patch and one fragment can
|
| 1266 |
+
// never sit in two, and neither rule needs a message: it is what
|
| 1267 |
// putting a piece of paper down on a table already does.
|
| 1268 |
+
for (const h of HOUSES) if (h.filed === holding) h.filed = null;
|
| 1269 |
+
nearDoor.filed = holding;
|
| 1270 |
audio?.mark?.();
|
| 1271 |
|
| 1272 |
// AND IF IT WAS THEIRS, THEY GO HOME.
|
|
|
|
| 1276 |
// there is nowhere for it to drift to.
|
| 1277 |
//
|
| 1278 |
// NOTHING IS SAID. No tick, no sound of being right, no change to the
|
| 1279 |
+
// patch. Somewhere out in the village a body gets up, and the only way
|
| 1280 |
// to find out is that it walks up the street.
|
| 1281 |
+
if (nearDoor.index === holding) {
|
| 1282 |
+
const owner = drained.find((d) => d.kept === holding && d.keptTaken);
|
| 1283 |
+
if (owner) { nearDoor.settled = true; owner.sendHome(nearDoor.x); }
|
| 1284 |
}
|
| 1285 |
}
|
| 1286 |
}
|
|
|
|
| 1306 |
// fragment, and this needs a letter still lying in a lit room. Ahead of the
|
| 1307 |
// object at his feet because he is looking into the room, and because the
|
| 1308 |
// clear pocket round every door keeps drops out of this spot anyway.
|
| 1309 |
+
if (wantsPickup && state.canTakeLetter && !prince.attacking) {
|
| 1310 |
litLetter.letterTaken = true;
|
| 1311 |
filePage({ kind: 'letter', house: litLetter.index });
|
| 1312 |
audio?.mark?.();
|
|
|
|
| 1324 |
litKey.keyTaken = true;
|
| 1325 |
prince.keys++;
|
| 1326 |
audio?.mark?.();
|
| 1327 |
+
} else if (wantsPickup && !state.canMark && nearest && !prince.attacking) {
|
| 1328 |
nearest.keptTaken = true;
|
| 1329 |
run.taken++;
|
| 1330 |
prince.inventory.push({
|