File size: 97,759 Bytes
71eae78
 
 
 
 
 
 
 
 
 
 
 
 
 
2428d78
835b445
d16591c
71eae78
 
 
 
 
7d93bf0
 
71eae78
 
83e579b
 
 
 
 
 
 
 
75ec82b
 
83e579b
 
 
 
 
 
 
75ec82b
 
 
 
 
ce91c1d
2428d78
71eae78
 
83e579b
 
71eae78
 
 
9f89772
 
 
75ec82b
 
 
 
 
9f89772
 
 
 
71eae78
2428d78
83e579b
 
71eae78
 
 
 
 
 
 
 
 
 
 
2428d78
 
 
 
71eae78
 
 
 
 
 
 
 
 
 
 
 
 
2428d78
44bae7c
 
 
42ec301
 
 
 
 
71eae78
 
 
 
 
 
 
 
 
835b445
 
 
 
 
71eae78
 
 
 
 
 
d16591c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2428d78
 
 
 
 
 
 
 
71eae78
 
 
 
 
9986f05
 
 
71eae78
 
2428d78
 
71eae78
 
2428d78
 
 
71eae78
 
 
2428d78
 
 
 
 
 
9986f05
 
71eae78
 
 
 
7d93bf0
2428d78
 
 
 
 
 
 
71eae78
 
 
2428d78
 
 
 
 
 
 
 
 
 
 
 
45cc183
 
 
 
2428d78
 
 
 
 
 
45cc183
 
 
 
 
 
 
2428d78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
835b445
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2428d78
 
 
 
7d93bf0
2428d78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ad47ef2
 
 
7d93bf0
 
 
2428d78
 
7d93bf0
 
2428d78
 
 
45cc183
 
 
 
 
 
 
 
 
7d93bf0
 
 
 
 
835b445
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7d93bf0
2428d78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a24e266
45cc183
 
835b445
a24e266
 
 
 
45cc183
 
 
 
 
 
 
 
 
 
 
 
 
835b445
 
 
 
 
 
 
 
 
 
 
 
a24e266
 
 
 
 
2428d78
 
 
 
 
 
818aea9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9986f05
 
 
45cc183
 
 
 
9986f05
 
 
45cc183
9986f05
45cc183
9986f05
45cc183
9986f05
 
 
 
 
 
 
 
 
 
 
 
2428d78
 
 
 
 
 
 
 
7d93bf0
2428d78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7d93bf0
 
2428d78
 
 
 
 
 
 
 
 
7d93bf0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2428d78
7d93bf0
 
 
 
2428d78
7d93bf0
2428d78
 
 
 
 
 
7d93bf0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45cc183
 
 
 
7d93bf0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2428d78
 
7d93bf0
2428d78
 
 
7d93bf0
 
2428d78
7d93bf0
 
 
 
 
 
2428d78
7d93bf0
 
 
 
 
2428d78
 
7d93bf0
2428d78
 
 
7d93bf0
 
 
 
2428d78
7d93bf0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71eae78
7d93bf0
2428d78
 
 
 
 
 
 
 
 
 
 
 
 
71eae78
 
 
44bae7c
 
71eae78
 
2428d78
44bae7c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b6d5a9
 
 
 
44bae7c
 
 
 
 
 
 
 
 
 
 
2428d78
 
 
 
 
 
 
 
835b445
2428d78
db17af1
 
 
 
 
 
7d93bf0
 
 
 
45cc183
7d93bf0
45cc183
 
 
 
7d93bf0
 
 
 
835b445
 
45cc183
 
 
152c254
 
 
835b445
7d93bf0
0b13ced
 
 
 
 
 
 
 
 
7d93bf0
0b13ced
 
 
7d93bf0
 
 
 
 
 
 
 
 
 
 
 
ad47ef2
 
 
 
 
 
 
7b6d5a9
 
 
 
ad47ef2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b6d5a9
 
 
ad47ef2
 
 
 
0b13ced
7d93bf0
 
45cc183
 
 
 
7d93bf0
 
f38d705
 
7d93bf0
 
 
 
0b13ced
 
 
 
 
 
7d93bf0
 
 
 
f38d705
 
 
7d93bf0
 
 
 
 
 
 
45cc183
 
 
 
7d93bf0
 
 
 
 
 
 
ad47ef2
 
 
 
 
 
 
45cc183
 
ad47ef2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b6d5a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7d93bf0
 
45cc183
 
 
 
7d93bf0
45cc183
7d93bf0
44bae7c
 
 
 
 
 
 
 
 
 
 
 
 
 
7d93bf0
 
 
 
94ca139
71eae78
 
 
 
94ca139
71eae78
 
7d93bf0
835b445
818aea9
 
db17af1
7d93bf0
0b13ced
7d93bf0
32103e5
835b445
 
 
 
 
7d93bf0
2428d78
45cc183
 
 
 
 
 
7b6d5a9
 
 
 
45cc183
 
 
2428d78
3647d73
9f89772
71eae78
2428d78
ad47ef2
2428d78
71eae78
 
7d93bf0
 
 
 
 
 
71eae78
 
 
 
44bae7c
835b445
71eae78
 
 
 
 
a24e266
 
835b445
a24e266
45cc183
 
 
 
7b6d5a9
 
 
835b445
71eae78
 
 
 
 
 
 
0b13ced
2428d78
 
 
835b445
db17af1
 
 
 
0b13ced
7d93bf0
 
 
 
 
 
45cc183
2428d78
44bae7c
 
 
94ca139
71eae78
 
ad47ef2
 
 
 
 
 
 
 
71eae78
ad47ef2
 
71eae78
 
 
 
45cc183
 
 
 
71eae78
 
 
 
ad47ef2
 
 
 
 
 
 
 
7b6d5a9
ad47ef2
7b6d5a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ad47ef2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b6d5a9
 
 
 
 
ad47ef2
 
 
 
71eae78
 
 
 
 
 
 
 
 
 
42ec301
 
 
71eae78
 
 
 
 
 
 
 
 
44bae7c
 
 
 
 
 
 
71eae78
 
 
 
 
 
 
2428d78
 
0b13ced
152c254
 
 
 
 
75ec82b
71eae78
98fd5ba
 
 
 
71eae78
42ec301
 
 
 
 
 
 
 
 
71eae78
 
 
 
 
 
 
 
 
 
 
 
 
 
2428d78
 
45cc183
 
 
 
 
 
 
 
 
2428d78
71eae78
45cc183
71eae78
 
 
 
 
 
 
 
 
94ca139
71eae78
 
 
44bae7c
 
 
 
71eae78
 
 
 
 
 
d16591c
 
 
 
 
 
 
71eae78
 
 
 
94ca139
71eae78
 
 
 
 
 
 
 
 
 
 
 
835b445
 
 
 
 
 
 
 
 
 
 
9986f05
 
 
 
71eae78
 
 
 
94ca139
835b445
152c254
d16591c
71eae78
d16591c
 
 
 
 
 
 
 
94ca139
835b445
 
 
 
 
 
 
 
 
 
 
 
d16591c
 
 
 
 
 
 
 
 
 
 
 
 
 
94e856b
 
 
 
 
 
 
 
9986f05
 
 
 
 
 
 
 
 
 
 
 
 
152c254
 
 
 
71eae78
2428d78
 
 
44bae7c
 
 
 
 
 
2428d78
71eae78
 
44bae7c
71eae78
 
 
2428d78
 
 
 
 
 
 
7d93bf0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b13ced
7d93bf0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2428d78
 
7d93bf0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b13ced
7d93bf0
 
 
 
 
2428d78
 
 
 
 
 
45cc183
 
 
 
 
 
2428d78
 
45cc183
 
7d93bf0
9986f05
45cc183
9986f05
45cc183
9986f05
7d93bf0
45cc183
 
 
 
 
 
1c99eda
45cc183
 
 
 
1c99eda
45cc183
 
 
1c99eda
45cc183
 
 
 
9986f05
45cc183
9986f05
1c99eda
835b445
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7d93bf0
 
45cc183
 
 
 
 
 
 
 
7d93bf0
45cc183
 
 
7d93bf0
45cc183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7d93bf0
 
 
45cc183
 
 
 
0b13ced
7d93bf0
 
45cc183
 
a24e266
7d93bf0
45cc183
 
 
 
 
 
 
 
 
 
 
 
7d93bf0
 
 
7b6d5a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7d93bf0
 
 
 
 
 
45cc183
 
 
 
7d93bf0
45cc183
 
 
835b445
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7d93bf0
6f7f7b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45cc183
 
 
 
 
835b445
 
45cc183
 
6f7f7b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45cc183
 
 
 
 
 
 
 
 
 
 
32103e5
6f7f7b5
 
45cc183
835b445
 
 
 
 
6f7f7b5
 
 
 
 
 
 
 
 
835b445
 
6f7f7b5
 
 
 
835b445
6f7f7b5
 
835b445
 
 
 
 
 
 
 
 
6f7f7b5
835b445
6f7f7b5
 
db17af1
 
 
 
 
 
 
 
 
 
9986f05
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
835b445
 
2428d78
 
71eae78
 
 
 
 
 
 
 
 
 
 
 
835b445
 
 
 
45cc183
835b445
 
 
 
 
 
f38d705
 
 
 
 
 
835b445
 
 
 
 
 
f38d705
 
 
 
 
 
 
 
 
 
 
94e856b
6f7f7b5
71eae78
44bae7c
71eae78
7d93bf0
71eae78
7d93bf0
 
 
71eae78
 
44bae7c
 
 
 
 
 
 
 
 
db17af1
 
 
 
 
 
 
 
 
 
152c254
 
 
 
 
 
 
 
 
 
 
db17af1
 
 
 
 
2428d78
835b445
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2428d78
 
 
45cc183
 
 
 
2428d78
 
 
 
 
 
71eae78
 
 
 
 
 
2428d78
0b13ced
 
2428d78
 
 
71eae78
 
 
 
44bae7c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71eae78
 
94ca139
 
 
 
 
 
44bae7c
ce91c1d
 
 
 
 
44bae7c
 
 
 
 
 
 
 
71eae78
 
 
 
 
 
 
 
 
 
 
 
ad47ef2
 
 
 
71eae78
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
// The level. A walkable stretch of forest with a beginning, encounters and an
// end.
//
// THE ARGUMENT THIS LEVEL MAKES: looking is what starts every fight. A Drained
// that has never been lit never attacks. All of the combat here is caused by
// the player choosing to see. A player who never raises the torch walks the
// whole level, learns nothing, and finishes it β€” and that has to stay possible.
//
// L1 scope: he walks, he has a facing, the camera follows, and the torch locks
// him in place. Background is L2, combat is L4, watchers are L5.

import { createPlayer } from './anim.js';
import { createTorch } from './torch.js';
import { createCamera } from './camera.js';
import { createBackground, tintFrames } from './background.js';
import { createDrained, HOUSE_OF, WANDER, RUSH, DOWNED, RISING, HOMING, HOME } from './drained.js';
import { TUNING, BOWL, PALETTE, TORCH, AUDIO, startX, runSeed, mulberry32, FLASH, flashAt, atLuminance } from './tuning.js';

export const W = 1920;
export const H = 1080;
export const GROUND_Y = 880;

export async function createLevelScene(clips, { audio = null, seed = null, letters = null } = {}) {
  const SEED = seed ?? runSeed();
  const runClip = clips.prince_run;
  const idleClip = clips.prince_idle;

  // EVERY POSE FALLS BACK TO HER STANDING DRAWING.
  //
  // The generated stand-ins are gone β€” raising the torch, holding the sheaf,
  // standing ready, holding his side. Each was a different body next to hers,
  // and changing pose looked like changing character, which is a worse lie than
  // not changing pose at all.
  //
  // AND NOW SHE HAS DRAWN THE REST: the swing, the fall and the torch held up.
  // Nothing he does is generated any more.
  //
  // Put any of them back in the manifest and it is used again with no code
  // change. That is the whole point of this file reading the manifest instead
  // of naming frames.
  const pose = (clip) => clip ?? idleClip;
  const torchClip = pose(clips.prince_torch);
  const guardClip = pose(clips.prince_guard);
  // HOLDING THE SHEAF HOLDS THE TORCH UP, so with no drawing of his papers in
  // his hand, the pose is the torch held up -- her arm -- and the sheet is drawn
  // at its mapPivot. Falling back to the standing drawing put the jointed
  // placeholder arm back on him every time he read.
  const mapClip = clips.prince_map ?? torchClip;
  const hurtClip = clips.prince_hurt ?? null;
  const sheetClip = clips.map_sheet;

  // The hand the torch is held in, published by the manifest in sprite
  // coordinates. Move the pose in the art and the light follows it β€” and with
  // no raised-torch drawing, that is her far hand at hip height.
  const hand = torchClip.handPivot ?? [torchClip.pivotX, torchClip.pivotY - 230];
  const HAND_DX = hand[0] - torchClip.pivotX;
  const HAND_DY = hand[1] - torchClip.pivotY;
  // The jointed placeholder arm, if the manifest has one. Its down pose lands
  // on handPivot to the pixel, so with or without it the lowered hand is in
  // the same place and only the raise differs.
  // Where the fire stands in each frame of her raise, as offsets from the pose's
  // pivot. Null for a pose with one hand position, which uses HAND_DX/DY.
  const TORCH_HANDS = torchClip.handPivots
    ? torchClip.handPivots.map(([x, y]) => [x - torchClip.pivotX, y - torchClip.pivotY])
    : null;
  const TORCH_ARM = torchClip.torchArm
    ? { ...torchClip.torchArm,
        shoulder: [torchClip.torchArm.shoulder[0], torchClip.torchArm.shoulder[1]] }
    : null;

  const START_X = startX();
  const attackClip = pose(clips.prince_attack);
  const downClip = pose(clips.prince_down);
  const ATTACK_TOTAL = (TUNING.ATTACK_WINDUP_MS + TUNING.ATTACK_ACTIVE_MS + TUNING.ATTACK_RECOVERY_MS) / 1000;
  const CONTACT_REACH = 110;

  /** Deterministic 0..1 from an integer. Same walk, same encounters. */
  const hash01 = (i, salt) => {
    let h = Math.imul((i | 0) ^ salt, 2654435761) >>> 0;
    h ^= h >>> 15; h = Math.imul(h, 2246822507) >>> 0; h ^= h >>> 13;
    return (h >>> 0) / 4294967296;
  };

  const drained = [];
  // Bounded at both ends from the first frame: the start of the road, and the
  // shut gate. See the note at GATE_CAM_X for why the right-hand bound is not
  // simply the gate.
  const camera = createCamera(W, 0, 0);   // real bounds set once GATE_CAM_X exists
  const torch = createTorch();

  const prince = {
    x: START_X, prevX: START_X,
    facing: 1,
    moving: false,
    hasSword: true,
    clip: idleClip,
    frame: 0,
    run: createPlayer(runClip),
    idle: createPlayer(idleClip),
    torchPlayer: createPlayer(torchClip),
    guard: createPlayer(guardClip),
    mapPlayer: mapClip ? createPlayer(mapClip) : null,
    hurtPlayer: hurtClip ? createPlayer(hurtClip) : null,
    // Hits TAKEN, not left. There is no bar; it is read off his body.
    hits: 0,
    // HOW FAR THE LIGHT HAS GONE OUT OF HIM. 0 is a full white eye, 1 is dark.
    // This is the health readout, and it replaces nothing because there never
    // was one: no bar, no number. He limps, he holds his side, the swing slows,
    // and now the one thing you can always see on a black silhouette dims.
    eyeDim: 0,
    attacking: false,
    attackTime: 0,
    attackConnected: false,
    attack: createPlayer(attackClip),
    down: createPlayer(downClip),
    // What he is carrying. Each entry knows whether he ever actually LOOKED at
    // it: take one in the dark and you own the thing without ever learning
    // whose it was, which is the level's whole argument stated as an object.
    inventory: [],
    // KEYS ARE NOT IN THE SHEAF and this is why they are a number rather than
    // a page. They carry no deduction, they cannot be read, and they are the
    // same object five times over. A pile of loose paper is what he has worked
    // out; this is just what is in his pocket.
    keys: 0,
  };

  // Async because the sky texture and the tinted hills are built once, here,
  // rather than per frame.
  const background = await createBackground(clips, GROUND_Y, H);

  // THE BACK LANE'S VALUE, baked once. Everything a Drained can be showing
  // while it stands beside the road β€” the still pose, the three loops, the
  // walk a wanderer paces on and steps down with β€” and the page a carrier
  // holds, because the page is part of the figure's mass in the dark.
  //
  // A tint and not a translucent black. Black at 86% would be 14 against the
  // hills and 21 against the sky: a figure you can see the world through.
  // `back` sits beside `dark` and `lit` on every frame, and the renderer
  // blends it toward `dark` while one steps down.
  {
    const hex = atLuminance(PALETTE.WATCHERS, TUNING.BACK_LANE_VALUE);
    for (const name of ['drained_watcher', 'drained_walk', 'drained_step',
      'loop_swing', 'loop_haul', 'loop_bear', 'kept']) {
      const c = clips[name];
      if (!c || c.frames[0]?.back) continue;
      const tinted = await tintFrames(c, hex, 'dark');
      c.frames.forEach((f, i) => { f.back = tinted[i]; });
    }
  }

  // Three Drained along the road, deterministic, NONE of them hostile until the
  // player makes them hostile. A player who never raises the torch walks the
  // whole level, learns nothing, and finishes it. That has to remain possible β€”
  // it is the fed run in miniature.
  //
  // They stand. Nothing in this level wanders any more: an unlit Drained is a
  // person who stopped, and standing still is what that looks like. See the
  // note at stillClip in drained.js.
  //
  // Each one gets a window and stands at the spot inside it with the most open
  // sky behind the action. A silhouette only shows up against light, so this is
  // the one thing Blade Warrior does that costs nothing β€” the fight belongs
  // under the bright patch. See background.skyOpenAt for what "open" counts.
  // THE TREE GOES DOWN FIRST, the way the doors do, and the three road drained
  // go in what is left. See TREE_CLEAR.
  const TREE_X = Math.round(TUNING.LEVEL_LENGTH_PX * TUNING.TREE_AT);
  {
    const L = TUNING.LEVEL_LENGTH_PX;
    const n = TUNING.ROAD_ENCOUNTERS;
    for (let i = 0; i < n; i++) {
      // The jitter moves the WINDOW, not the answer. Applying it after the
      // choice simply undid the choice.
      const centre = (0.25 + i * (0.5 / Math.max(1, n - 1))) * L +
        (hash01(i, 0x51) - 0.5) * 500;
      const span = 0.08 * L;
      let bestX = Math.round(centre), best = -Infinity;
      for (let k = -12; k <= 12; k++) {
        const cand = centre + (k / 12) * span;
        if (cand < 900 || cand > BOWL.X0 - 900) continue;
        // Never within the torch's own WIDTH of one already placed. Chasing the
        // open sky pulled two of three encounters to 667px apart, and the torch
        // is 680 across β€” so one raise woke both, on the stretch that is
        // supposed to be teaching him what raising it costs.
        if (drained.some((o) => Math.abs(o.x - cand) < TORCH.RADIUS * 2)) continue;
        // And never within the torch's reach of the note on the tree.
        if (Math.abs(cand - TREE_X) < TUNING.TREE_CLEAR) continue;
        // Gentle pull to the middle of the window so ties do not drift to an end.
        const score = background.skyOpenAt(cand, W) - Math.abs(cand - centre) * 0.6;
        if (score > best) { best = score; bestX = Math.round(cand); }
      }
      // They carry NOTHING and they do no loop at all. They are from elsewhere:
      // travellers who stopped, not residents. One object per kind means a
      // second hammer out here would file against the forge and the map's only
      // error state is two objects landing on one house.
      //
      // So the loop skill correctly rules them out, once the player has it.
      drained.push(createDrained(clips, bestX, hash01(i, 0x77) < 0.5 ? -1 : 1, i,
        { kept: null, keptMaxX: BOWL.X0 }));
    }
  }

  // --- the village ------------------------------------------------------
  // What used to be here: thirty-four watchers stepping onto the path until he
  // could not get through. A placeholder from the first day, and deleted rather
  // than disabled. The background watcher layer already populates the bowl, and
  // a second non-interactive crowd system beside it was one system too many.
  //
  // The mouth is a one-way threshold. Cross it and the camera stops following
  // and starts being bounded; walking back inside is free, walking back out is
  // not. There is no art at the mouth and no fade. The houses are the only
  // thing that says the mode changed.
  const GATE_X = BOWL.X1;
  const GATE_STOP_X = GATE_X - 380;      // inside the gateway, clear of the post
  // AND THE FAR SIDE OF THE SAME GATEWAY, once the man who holds it has gone
  // home. 40px inside the right post, as GATE_STOP_X is inside the left, so he
  // never stands in front of pure black and disappears into it.
  const GATE_ROOM_X = GATE_X + 380;
  // Where the view stops, and it is not simply the gate. Bounding the camera at
  // GATE_X - W/2 put the wall across the middle of the frame and shoved him
  // into the left half to stand at it. Stopping three quarters of the way over
  // leaves the village behind him visible while he is working out that the gate
  // will not open, which is the exact moment he has to turn around.
  const GATE_CAM_X = GATE_X - W * 0.75;
  // AND A LITTLE FURTHER ONCE THE GATEWAY IS HIS. With its keeper home he can
  // walk to the far post, and at GATE_CAM_X that stood him 100px from the edge
  // of the frame with the post cut in half. This frames the whole gateway β€”
  // both posts, the leaf and all four sockets β€” with 100px of wall past it.
  // The keeper leaves while the prince is at the gatehouse, a screen away, so
  // the view is never at either bound when it changes and nothing jumps.
  const GATE_ROOM_CAM_X = GATE_X + 620 - W;

  // THERE IS NO ONE-WAY MOUTH, and the reason is worth keeping.
  //
  // The spec called the threshold at the village's edge "the whole transition":
  // cross it and the camera stops following and starts being bounded, and you
  // cannot walk back out. That was written when the road was twelve screens of
  // empty approach nobody would ever want to re-walk.
  //
  // The road is four screens now and has three Drained on it. Locking the
  // threshold behind him does not focus the level, it deletes three encounters
  // and everything they were carrying the moment he steps past a house. So the
  // camera is bounded at BOTH ends of the whole level from the first frame, and
  // the village is a place rather than a room with a door.
  //
  // What the threshold was FOR survives without it: the houses are still the
  // only signal the mode changed, and the shut gate is still the only way out.
  camera.setBounds(0, GATE_CAM_X);

  // --- the street --------------------------------------------------------
  // Five houses and eleven shapes, laid out as a RHYTHM rather than a scatter.
  //
  // The village is divided into five blocks. Each block is a house at its head
  // and open ground behind it, and the shapes live in the open ground. That
  // guarantees the clear pocket around every door by construction, instead of
  // scattering shapes and hoping none of them lands on a doorstep β€” which is
  // what the first pass did, and all five doors were unlookable.
  //
  // It also gives the place a shape you can read while walking: a building, a
  // stretch of people, a building. A village street, not a field of posts.
  const CLEAR = TUNING.BOWL_CLEAR;
  // THE GATEHOUSE COMES OUT OF THE ROW AND STANDS BESIDE THE GATE, which is
  // what its own art note has said since it was drawn: "palace-built, like the
  // gate it stands beside". It was 2400px away, which is off the right edge of
  // a pinned camera β€” so filing the gatekeeper's fragment, the first thing
  // anybody files and the tutorial for the entire ending, showed nothing at all
  // for the better part of a minute.
  //
  // At 1100px short of the gate his walk home is 870px: he is 210px off the
  // right edge when the button is pressed and in frame within a second. His
  // house is still not the gate and he still walks away from the post, which
  // is the whole of what section 8 is about. It just happens where you can see
  // it. The facade is 960 wide, so it ends at 27380 and the gate's posts begin
  // at 27580; the torch on its door reaches 27100, which is 670px short of him
  // and comfortably outside DOOR_CLEAR, so lighting it still wakes nothing.
  const GATEHOUSE_X = BOWL.X1 - 1100;
  const SPAN4 = (GATEHOUSE_X - 1600) - (BOWL.X0 + 500);
  const houseX = (i) => (i === 4 ? GATEHOUSE_X
    : Math.round(BOWL.X0 + 500 + SPAN4 * (i + 0.5) / 4));

  // Lighting a door tells you who lived there and nothing else β€” no shaped
  // gaps, no racks, no missing-tool puzzle.
  //
  // Four of the five have someone out in the forest carrying their answer. The fifth
  // is not anywhere, and the game never once says so.
  //
  // SIZED AGAINST A MAN, not picked. At 470 tall a house came out 1.8x a
  // standing figure, and a building a man nearly fills the door of reads as a
  // shed. These run 1.9x to 2.3x and are wider than they are tall.
  //
  // AND THEY ARE NOT PURE BLACK. Standing the prince in front of an 820px black
  // wall merged the two into one mass and he simply disappeared for two seconds
  // at a time β€” the same failure the watchers had, at ten times the area. So a
  // house takes the watcher treatment, which is already measured and already
  // tested: luminance 26 rather than 0. Everything ON the road stays pure black
  // and reads against them. Its foot still meets the ground β€” a building
  // floating above the ground line reads as a mistake, and the VALUE is what
  // says it is set back, not a gap under it.
  //
  // The gate is the exception and stays pure black, because he never reaches
  // it β€” he stops 380px short and the wall is the thing that stopped him.
  const houseClip = clips.houses;
  // Where his orders came from. Not one of the five, and deliberately not a
  // sixth variant of `houses` -- see the manifest's note on the palace clip.
  const palaceClip = clips.palace ?? null;
  // Index order is house order and the names come straight off HOUSE_OF, so a
  // fragment's house and a plot on the map are the same number and cannot
  // drift apart.
  const HOUSES = ['forge', 'bakehouse', 'wellhouse', 'home', 'gatehouse'].map((kind, i) => ({
    kind,
    index: i,
    answers: HOUSE_OF[i],
    variant: i,
    x: houseX(i),
    w: houseClip ? houseClip.width : 960,
    // HAS THE TORCH BEEN ON ITS DOOR OR ITS LETTER? Latches, like the objects
    // on the ground, and for the same reason. It is the whole of what the door
    // remembers about him: the band reads a lit door again for free, and the
    // slot takes a pin.
    //
    // THE LETTER NEVER COMES DOWN, so there is no second flag for it. Letters
    // stay where they were left and fragments come off bodies: he does not
    // pocket a stranger's mail, and the door is where he judges.
    seen: false,
    // WHAT HE HAS FILED UNDER THIS HOUSE, and nothing else in the build writes
    // it. The `kept` index of one fragment, or null. The map used to fill this
    // in by itself the moment he looked at something, which made it a receipt;
    // a claim only means anything if it can be wrong, so he makes it.
    filed: null,
    // SOMEBODY CAME HOME TO IT. Set when a body finishes its walk at this
    // door, and it is the only thing in the level that is ever a verdict β€”
    // except that it is not one, because the game does not say it. A person
    // is standing in the doorway who was not standing there before.
    open: false,
    // The key off its hook. One per house, and four of the five can be had.
    keyTaken: false,
    // THE FRAME IS FINISHED WITH. Set the instant a correct fragment goes in,
    // which is the instant its owner starts walking, not when it arrives.
    //
    // It is not a cost. Brute-forcing a door is still four presses and the
    // addendum allows it. What it stops is one thing: taking a name back off a
    // wall after the person it belonged to is already on their way, which
    // would either have to turn them round or quietly not.
    settled: false,
  }));

  // The door, as a world point, from the clip's own pixels. Move it in the art
  // and the reveal moves with it β€” the same deal keptAnchor and handPivot get.
  //
  // No fallback. A missing doorAt used to quietly default to 140px above the
  // ground, which happened to be the exact value the manifest asks for β€” so the
  // field was not being read at all and nothing showed it. Throwing is the same
  // guard tintFrames got after it silently painted the previous colour.
  if (houseClip && !houseClip.doorAt) {
    throw new Error('clip "houses" has no doorAt: nothing would say where the door is');
  }
  const doorOffset = houseClip
    ? [houseClip.doorAt[0] - houseClip.pivotX, houseClip.doorAt[1] - houseClip.pivotY]
    : [0, 0];
  const doorPoint = (h) => [h.x + doorOffset[0], GROUND_Y + doorOffset[1]];

  /**
   * The middle of the letter beside that door, as a world point. It never
   * comes down, so this is always where it hangs.
   *
   * Straight out of the clip's own pixels, the same deal `doorAt` gets: move it
   * in the art and the thing the torch has to cover moves with it.
   */
  const letterPoint = (h) => {
    const r = houseClip?.letterAt?.[h.variant];
    if (!r) return null;
    return [h.x - houseClip.pivotX + r[0] + r[2] / 2,
      GROUND_Y - houseClip.pivotY + r[1] + r[3] / 2];
  };

  /**
   * And the middle of the slot under it: the second nail, the lighter patch
   * where their own paper hung, and the page he pins back into it. Same column
   * as the letter, so the reach to either is the same reach.
   */
  const slotPoint = (h) => {
    const r = houseClip?.slotAt?.[h.variant];
    if (!r) return null;
    return [h.x - houseClip.pivotX + r[0] + r[2] / 2,
      GROUND_Y - houseClip.pivotY + r[1] + r[3] / 2];
  };

  /**
   * The middle of the key on its hook, as a world point. Same deal doorAt and
   * letterAt get: move it in the art and what the player has to walk to moves
   * with it.
   */
  const keyPoint = (h) => {
    const r = houseClip?.keyAt?.[h.variant];
    if (!r) return null;
    return [h.x - houseClip.pivotX + r[0] + r[2] / 2,
      GROUND_Y - houseClip.pivotY + r[1] + r[3] / 2];
  };

  // The dark pass wants them at the watcher value, not black β€” see the note
  // above. Tinted once here rather than per frame, like the watchers are.
  const houseArt = houseClip
    ? await tintFrames(houseClip, PALETTE.WATCHERS, 'dark')
    : null;

  // --- holes the shape of what goes in them --------------------------------
  //
  // The patch beside a door was a rectangle and the gate's sockets were
  // rectangles, and a rectangle says "a place" without saying what for. Cut
  // them to the shape of the thing instead and they say both at once, with no
  // words and no prompt: a torn page fits in a torn-page-shaped mark, a key
  // fits in a key-shaped hole. It is the oldest trick there is and this level
  // had been asking its question in the vaguest possible shape.
  //
  // TINTED ONCE AT LOAD, off the real art, so the hole and the thing can never
  // drift apart β€” the same treatment the houses and the watchers get. All five
  // torn pages share one contour by design and the silhouette pass throws the
  // scribble away, so frame 0 is every fragment's outline.
  const PATCH_HEX = '#6f6553';            // lum 101 β€” the cleaner wall. See render.js
  const patchArt = clips.kept
    ? (await tintFrames(clips.kept, PATCH_HEX, 'dark'))[0] : null;
  const socketArt = clips.key
    ? (await tintFrames(clips.key, PALETTE.WATCHERS, 'dark'))[0] : null;

  // --- the tree on the road ------------------------------------------------
  // A rebel nailed the rule to it: they cannot speak, what they carried is all
  // that is left of them, take it home. Never an answer β€” not which ones, not
  // which home. It reads like a house's letter in every way the code cares
  // about: light it and the band reads it, and after that standing by it
  // does. It stays on the tree, the same as every letter stays on its door,
  // and it is not a door β€” no slot, and nothing is ever pinned back to it.
  const treeClip = clips.road_tree ?? null;
  const TREE_NOTE = letters?.byHouse?.findIndex((h) => h.origin === 'tree') ?? -1;
  const TREE = treeClip && TREE_NOTE >= 0
    ? { kind: 'tree', x: TREE_X, index: TREE_NOTE, variant: 0, seen: false }
    : null;
  /** The middle of the note on the tree, as a world point. Off the clip's pixels. */
  const treePoint = () => {
    const r = treeClip?.letterAt?.[0];
    if (!TREE || !r) return null;
    return [TREE.x - treeClip.pivotX + r[0] + r[2] / 2,
      GROUND_Y - treeClip.pivotY + r[1] + r[3] / 2];
  };
  // PALE IN THE DARK. A house letter has a lit doorway beside it to draw the
  // eye; a note on a lone tree has nothing, and would be walked past. So the
  // scrap shows as paper catching what light there is β€” enough to say
  // "something is pinned here", never enough to read. Above the no-mud floor.
  const NOTE_IN_THE_DARK = '#9a9484';     // lum 148
  const treeNoteDark = TREE && clips.letter
    ? (await tintFrames(clips.letter, NOTE_IN_THE_DARK, 'dark'))[TREE_NOTE] ?? null : null;

  // --- the twelve --------------------------------------------------------
  // Twelve shapes you can swing at, against the background watchers you cannot.
  // Two populations doing different jobs: the watchers make it feel like a
  // village and cost nothing, and these are what you walk to.
  //
  // Eleven stand out in the village and the twelfth is the gatekeeper, at his
  // post sixty years after anyone relieved him.
  //
  // V1 CARRIES NOTHING. The four carriers, the loops and the one-in-three
  // ratio that makes the search a search are V4; what V1 is for is the size of
  // the room, and objects in it would only be something to look at instead.
  //
  // THE DOORS ARE PLACED FIRST AND THE SHAPES GO IN WHAT IS LEFT.
  //
  // Holding the torch freezes him and takes his sword away, so a shape inside
  // the light's reach of a door means looking at that door wakes something with
  // nowhere to back off to. The first village scattered shapes evenly and
  // checked afterwards: all five doors came out unlookable, one of them with a
  // Drained 59px away. So the clear pockets are cut out of the street FIRST,
  // and the eleven are spread evenly through the ground that remains.
  //
  // Even in FREE-SPACE coordinates, not world ones. Walking a straight line
  // through the gaps and dropping a shape every so often is what keeps them
  // from bunching up in whichever gap happened to be largest β€” which is exactly
  // how the per-block version put three of them 591px apart.
  const free = (() => {
    const out = [];
    let at = BOWL.X0 + 400;
    for (const h of HOUSES) {
      if (h.x - CLEAR > at) out.push([at, h.x - CLEAR]);
      at = Math.max(at, h.x + CLEAR);
    }
    // Short of the gate: the gatekeeper's ground is his, and a shape loitering
    // there would be in the fight that is supposed to be his alone.
    if (GATE_STOP_X - CLEAR > at) out.push([at, GATE_STOP_X - CLEAR]);
    return out;
  })();
  const FREE_LEN = free.reduce((n, [a, b]) => n + (b - a), 0);

  /** A distance along the free ground, as a world x. */
  const atFree = (u) => {
    let left = u;
    for (const [a, b] of free) {
      if (left <= b - a) return a + left;
      left -= b - a;
    }
    return free[free.length - 1][1];
  };

  // WHO IS DOING WHAT, and it is a fixed pattern rather than a roll.
  //
  // Three motions are out here β€” the gatekeeper is the fourth carrier and he is
  // at the gate. Each motion is done by three shapes: one carrying, two not. The
  // last two do nothing in particular, which is what gives the loop skill
  // something to correctly rule OUT. That is how a skill gets taught without
  // being taught.
  //
  //   hammer   1 carrier + 2 decoys        yoke      1 carrier + 2 decoys
  //   peel     1 carrier + 2 decoys        nothing   2 decoys
  //
  // Eleven. Four fights if reading outlines has landed, twelve if it has not,
  // and that spread IS what V4 measures.
  // WHO DOES WHAT, AND THE LOOP IS A LAST MOMENT.
  //
  // The drained are still doing whatever they were doing when she took them.
  // That is not a life: a blacksmith swings a hammer for thirty years, and on
  // the morning she came he was hauling water because his brother had a fever.
  // The house letter says who he was, the loop says what he was doing at the
  // end, and the gap between the two is the deduction.
  //
  //   swinging   3 shapes, and NONE of them carries. The swinger is bait, and
  //              the forge letter names this motion on purpose
  //   hauling    3 shapes, one carrying β€” the forge's own fragment
  //   bearing    3 shapes, two carrying β€” the bakehouse and the well house,
  //              who were both doing the same thing that morning
  //   wandering  2 shapes doing nothing anybody wrote down
  //
  // THE LAST TWO ARE THE NEGATIVE CONTROL. Without something the loop skill can
  // correctly rule OUT, every shape is worth a fight and watching is
  // decoration. They wander rather than stand, because standing reads as the
  // gatekeeper or as a watcher β€” their motion has to be legibly not a trade.
  const ROLES = [
    { loop: 'swing' }, { loop: 'swing' }, { loop: 'swing' },
    { loop: 'haul' }, { loop: 'haul' }, { loop: 'haul' },
    { loop: 'bear' }, { loop: 'bear' }, { loop: 'bear' },
    { roams: true }, { roams: true },
  ];

  const BOWL_N = ROLES.length;
  // The furthest a dropped object may land and still be pickable. Handed to
  // every Drained, because the one that got this wrong was unreachable by
  // 375px and the run simply could not be finished.
  const KEPT_MAX_X = GATE_STOP_X - TUNING.PICKUP_REACH * 0.6;

  const village = [];
  const SLOTS = [];

  /**
   * How far a wanderer at x may pace without ever entering a door's pocket.
   *
   * Bounded PER DIRECTION. Taking the nearest house in any direction and
   * applying it both ways leaves the far side under-constrained, and measuring
   * across a dozen seeds it came within 2px of the clearance β€” passing, but on
   * nothing. The house on the left decides how far left it goes.
   */
  const roamRoom = (x) => {
    const left = HOUSES.filter((h) => h.x < x).map((h) => x - h.x - CLEAR);
    const right = HOUSES.filter((h) => h.x >= x).map((h) => h.x - x - CLEAR);
    return Math.max(0, Math.min(
      (FREE_LEN / BOWL_N) * 0.4,
      ...(left.length ? [Math.min(...left)] : []),
      ...(right.length ? [Math.min(...right)] : []),
    ));
  };

  {
    const slot = FREE_LEN / BOWL_N;
    for (let i = 0; i < BOWL_N; i++) {
      // The slots themselves are WORLD-seeded and never move: their clear
      // pockets round each door and their spacing floor are what V1 bought.
      const u = slot * (i + 0.5) + (hash01(i, 0xb0a1) - 0.5) * slot * 0.2;
      SLOTS.push(Math.round(atFree(u)));
    }
    ROLES.forEach((role, i) => {
      village.push(createDrained(clips, SLOTS[i], hash01(i, 0xb0a2) < 0.5 ? -1 : 1, i,
        { standing: !role.roams, stayDown: true, loop: role.loop ?? null,
          kept: null, keptMaxX: KEPT_MAX_X,
          // A wanderer stays in the open ground it was placed in. Let one drift
          // and it walks into a door's clear pocket, and lighting that door
          // wakes it β€” which is the one thing V1's placement bought.
          //
          // BOUNDED BY THE DOOR, NOT ONLY BY THE SLOT. Half a slot is 370px,
          // and a slot only has to be BOWL_CLEAR from a door to be legal β€” so
          // a wanderer in the tightest slot could pace 370 toward the door and
          // end up well inside the pocket. Which slot a wanderer lands in
          // varies per run, so this only started happening when roles began
          // moving between slots.
          roam: role.roams ? roamRoom(SLOTS[i]) : 0 }));
    });
    // The gatekeeper. Inside the opening, against the shut leaf, and within the
    // prince's 175px reach of where the gate stops him. He never leaves.
    village.push(createDrained(clips, GATE_X - 230, -1, BOWL_N,
      { standing: true, stayDown: true, post: true, kept: HOUSE_OF.indexOf('the gatehouse'),
        keptMaxX: KEPT_MAX_X }));
  }
  // Whether his post is empty: walking home, or home. Nothing else takes him
  // off it β€” a fight leaves him on the ground where he stood.
  const GATEKEEPER = village[village.length - 1];
  const keeperGone = () => GATEKEEPER.state === HOMING || GATEKEEPER.state === HOME;

  /**
   * THE DEAL, AND IT IS A CONSTRAINED SHUFFLE.
   *
   * The rules are the authored part of the level. Which specific body satisfies
   * one is not, and it is re-rolled every run so that four friends comparing
   * notes cannot say "the blacksmith is third from the left".
   *
   * It buys that and not a second puzzle: a player who remembers that the forge
   * letter subverts has spent the deduction, and this makes them fight again
   * rather than think again.
   */
  function dealVillage(seed) {
    const rnd = mulberry32(seed >>> 0);
    const shuffled = (list) => list.map((v) => [v, rnd()])
      .sort((a, b) => a[1] - b[1]).map((v) => v[0]);

    // --- which body stands in which slot ---------------------------------
    // Dealt so NO TWO NEIGHBOURS SHARE A MOTION. A clump of three identical
    // loops hands the player a spatial tell, and the point is that narrowing by
    // motion leaves candidates spread out with nothing to choose between them.
    const pool = new Map();
    ROLES.forEach((r, i) => {
      const key = r.loop ?? '-';
      if (!pool.has(key)) pool.set(key, []);
      pool.get(key).push(i);
    });
    for (const [k, list] of pool) pool.set(k, shuffled(list));
    const order = [];
    let last = null;
    for (let n = 0; n < BOWL_N; n++) {
      // Largest group first so nothing is left stranded at the end, and TIES
      // BROKEN AT RANDOM. Without the tie-break the greedy rule is fully
      // determined by group sizes, so every run laid the village out in the
      // same order of motions β€” swing, haul, bear, swing, haul, bear β€” and
      // "the fifth shape is a hauler" survived the shuffle intact.
      let best = null;
      let bestN = -1;
      for (const k of shuffled([...pool.keys()])) {
        const arr = pool.get(k);
        if (!arr.length || (k === last && pool.size > 1)) continue;
        if (arr.length > bestN) { best = k; bestN = arr.length; }
      }
      if (!best) best = [...pool.keys()].find((k) => pool.get(k).length);
      order.push(pool.get(best).shift());
      if (!pool.get(best).length) pool.delete(best);
      last = best;
    }
    order.forEach((body, slotIndex) => {
      const x = SLOTS[slotIndex];
      village[body].placeAt(x, roamRoom(x));
    });

    // --- who is carrying what --------------------------------------------
    for (let i = 0; i < BOWL_N; i++) village[i].setKept(null);
    const byLoop = (l) => shuffled(ROLES.map((r, i) => (r.loop === l ? i : -1)).filter((i) => i >= 0));
    // The forge's fragment goes on a HAULER, never a swinger. The whole
    // subversion rests on it: read "I could hear you at the anvil", walk to the
    // swinging shape, and nothing falls out of it.
    village[byLoop('haul')[0]].setKept(HOUSE_OF.indexOf('the forge'));
    // The bakehouse and the well house were both walking somewhere with
    // something that morning, so both go on BEARERS. Two of the three.
    const bearers = byLoop('bear');
    const pair = shuffled([HOUSE_OF.indexOf('the bakehouse'), HOUSE_OF.indexOf('the well house')]);
    village[bearers[0]].setKept(pair[0]);
    village[bearers[1]].setKept(pair[1]);
    // Home gets nothing, anywhere. That is the residue.
    // The gatekeeper keeps his own and is never re-dealt.
  }
  dealVillage(SEED);

  // What the layout actually produced, which is what V1 is judging and is NOT
  // the bowl width over twelve β€” the clear pockets around the doors eat it.
  const VILLAGE_GAP = (() => {
    const xs = village.map((v) => v.x).sort((a, b) => a - b);
    let sum = 0;
    for (let i = 1; i < xs.length; i++) sum += xs[i] - xs[i - 1];
    return Math.round(sum / (xs.length - 1));
  })();

  // One list. The village shapes are Drained like any other: the same three
  // hits, the same stagger, the same rules about being looked at.
  drained.push(...village);

  // The loss beat. No text, no shake, no sting β€” the light going out is the
  // whole thing.
  // The DEATH beat now, not the loss beat. Being caught costs a hit; the sixth
  // one is this, and it restarts the level.
  const loss = { active: false, timer: 0, torchWasUp: false, x: 0, y: 0, vx: 0, r: 0, by: null };
  let grace = 0;

  /**
   * Caught. Knocked back, a moment to get clear, and one less.
   *
   * WANDER contact is still harmless β€” the whole level argues that nothing is
   * hostile until it is looked at, and a man standing in the dark doing nothing
   * has to stay a man standing in the dark doing nothing.
   */
  function takeHit(fromX, knock) {
    if (grace > 0 || loss.active) return;
    prince.hits++;
    run.hitsTaken++;
    prince.x += (Math.sign(prince.x - fromX) || -1) * knock;
    prince.prevX = prince.x;
    prince.attacking = false;
    grace = TUNING.HURT_GRACE_MS;
    audio?.hit();
    if (prince.hits < TUNING.PRINCE_HITS) return;
    // The sixth. The light goes out and the level starts again -- and nobody
    // goes on reading to a dead man.
    audio?.stopVoice?.();
    voice.started = false;
    loss.active = true;
    loss.timer = 0;
    prince.down.reset();
    loss.torchWasUp = torch.raise > 0.01;
    loss.x = torch.handX;
    loss.y = torch.handY;
    loss.vx = prince.facing * 120;
    loss.r = torch.radius;
    audio?.bodyFall();
  }

  // THE MAP. Not a screen β€” a sheet he holds up in his own torchlight. It
  // records and it never reasons: it knows what he has looked at and nothing
  // else, and the player never does a moment's bookkeeping.
  // THE GATE. Shut, with four empty marks on it, and it does not open until he
  // is carrying four objects he has LOOKED at. Not four objects β€” take them
  // blind and he is holding four question marks and the gate stays shut.
  //
  // He has to look. That is the level.
  const gate = { filled: 0, open: 0, through: false, placed: 0 };

  // THE SKY, when somebody gets their name back. `ms` counts up through the
  // strike and `thundered` makes sure the rumble fires once rather than every
  // frame the flash is still running. See FLASH in tuning.js for why it is a
  // flash and not a sound.
  const flash = { ms: 0, thundered: true };

  /**
   * THE SHEAF. Loose pages he carries, and the map is one of them.
   *
   * Built here at L2 and paged at L3. What arrives when:
   *   his orders     at the start, and permanently page one
   *   a fragment     when he takes it
   *   a key          when he takes it off its hook
   *
   * NO HOUSE LETTERS AND NO NOTE. Those stay where they were left β€” on the
   * door and on the tree β€” and the band reads them there.
   *
   * FOUND ORDER. Not grouped, not sorted, no index and no tabs β€” the order he
   * picked them up in is the only order there is.
   */
  // Last frame's take button, so an act fires on the edge. See where it is read.
  let prevPickup = false;
  // How long he has stood still, in ms. The band reads a door he has lit again
  // once this passes READ_STILL_MS. See the band.
  let still = 0;
  // Whether he wanted the light last frame, so the flame fires on the raise
  // and not on every frame of holding it.
  let litLast = false;

  const sheaf = {
    // NO MAP PAGE. It was page one from V3 until somebody played it and said
    // "I thought the first page was just the inventory index of stuff β€” I did
    // not realise it was a map." Which is the whole verdict on it: the sheet
    // that the V3 gate was built around never read as a map to anybody.
    //
    // So it is what it was being read as. A pile of what he is carrying, in the
    // order he found it, and nothing else. A real map that draws itself as he
    // solves things is a good idea and a separate one.
    pages: [],
    index: 0,
    // Whether he is holding it up. Lived on its own `map` object until the name
    // stopped being true.
    up: false,
    // Edge and repeat for the arrows. A turn is a FLIP WITH A BEAT, not a free
    // browse: it fires on the press, then not again until REPEAT_MS, so holding
    // an arrow walks the pile at reading speed rather than spinning it.
    heldDir: 0,
    repeat: 0,
    // Counts up on every turn. The band watches it rather than watching the
    // text, because paging from one page to another with the SAME words has to
    // still read as a turn.
    turns: 0,
  };
  const SHEAF_REPEAT_MS = 260;

  /**
   * THE OPENING. He is reading his orders when the game starts.
   *
   * Not a splash and not narration: the page is in his hand, held up in the
   * light, in the exact posture the player will use for the whole game -- shown
   * before they know there is a button for it.
   */
  // `heard`: his orders have started being read aloud. `clear`: every key has
  // been let go since. `askedAt`: when the first press asked for the reading,
  // in opening ms, or -1. See the opening in update().
  const opening = { ms: 0, done: false, heard: false, clear: false, askedAt: -1 };

  /**
   * What `update` is handed while the beat runs, INSTEAD of the real input.
   *
   * The death beat owns its frame with a bare `return` placed above every
   * input read. Copying that here would freeze the fire: it returns above
   * `state.clock += dt` and above the flame player, which cost nothing when
   * the torch was four SVG frames and costs everything now that it is a
   * 25-frame loop and the only colour on screen. Four seconds of a frozen
   * flame at the front of the game is worse than no opening.
   *
   * So nothing is skipped. The rest of update runs on these flags instead: the
   * sheaf goes up, the torch stays up, the band shows the order, the pose swaps
   * to the map pose, the clock advances and the fire burns -- and he does not
   * move, because left and right are false. "No input is accepted" ends up
   * literally true rather than approximately true.
   */
  // NOT FROZEN, AND THAT COST A CRASH. The scene clears `input.pickup = false`
  // further down, every frame -- the edge detector was deliberately moved in
  // here so it would not trust the input layer. Object.freeze turns that write
  // into a TypeError under a module's strict mode, so the whole update died
  // mid-frame on the first frame of the game. It is a plain object because
  // something legitimately writes to it.
  const OPENING_INPUT = {
    torch: true, sheaf: true, attack: false, pickup: false, left: false, right: false,
  };

  /**
   * Arm it. Sets the raise to 1 rather than letting it ramp: RAISE_MS is 250
   * and the band needs raise > 0.9, so coming up through the input path gives
   * a quarter of a second of a page rising into view with no legible word on
   * it. The spec says he is ALREADY holding it up, so he is.
   */
  const armOpening = () => {
    opening.ms = 0;
    opening.done = false;
    opening.heard = false;
    opening.clear = false;
    opening.askedAt = -1;
    torch.raise = 1;
    torch.prevRaise = 1;
  };

  const page = () => sheaf.pages[sheaf.index] ?? null;

  /**
   * Has he looked at this page's contents? The one letter he carries is his
   * orders, which he is reading when the game starts. A FRAGMENT CAN BE TAKEN
   * BLIND and arrives unknown, and the whole gate at the end of the level
   * turns on that difference.
   */
  const pageSeen = (pg) => {
    // A letter was read off a lit door and a key has nothing written on it, so
    // only a torn page can be a thing he owns without having looked at it.
    if (!pg || pg.kind !== 'fragment') return true;
    return !!prince.inventory.find((it) => it.kept === pg.kept)?.seen;
  };

  /** Where this page is filed, if it is a fragment and he has filed it. */
  const filedUnder = (pg) => {
    if (!pg || pg.kind !== 'fragment') return null;
    return HOUSES.find((h) => h.filed === pg.kept) ?? null;
  };

  const textFor = (pg) => {
    if (!letters || !pg) return null;
    const h = letters.byHouse?.[pg.house ?? pg.kept];
    if (!h) return null;
    // A KEY HAS NOTHING WRITTEN ON IT, so the band stays empty on one β€” which
    // is the difference between a key and a letter, shown rather than said.
    // (This used to say "a map", back when page zero was one.)
    return pg.kind === 'letter' ? h.letter : pg.kind === 'fragment' ? h.fragment : null;
  };

  /**
   * Put a page on the pile and open on it.
   *
   * FOUND ORDER means it goes on the end. Moving the index to it is what a
   * person does with a handful of papers β€” the new one is the one in your
   * hand β€” and it is what makes pinning one gesture instead of two: take a
   * fragment off a body, walk to a door, press E, and it is already the page
   * he is holding.
   */
  const filePage = (pg) => {
    sheaf.pages.push(pg);
    sheaf.index = sheaf.pages.length - 1;
    sheaf.turns++;
  };

  /**
   * HIS ORDERS. Page one, from before the first frame, and permanently.
   *
   * The pile used to start empty and a check asserted it, under a comment
   * explaining that he begins carrying nothing. That was true of a pile of
   * things he had FOUND. This is the one page he did not find: it is what he
   * was handed at the palace and what sent him down this road, and by the end
   * of the village it sits under four fragments that, with the letters on the
   * doors they answer, contradict every word of it. Nothing has to say so.
   *
   * IT IS AN ORDINARY LETTER PAGE. Not its own kind: it is a page of writing
   * with a building behind it, which is what every letter in this game is, and
   * making it a fourth kind meant a branch in textFor, a branch in the hand,
   * and a row in every check that counts kinds -- all to say "paper" twice.
   * Its byHouse entry carries `origin: 'palace'`, and the index is FOUND by
   * that rather than counted to, so nobody has to keep a 5 in their head.
   *
   * Through filePage rather than a bare push, so it bumps `turns` -- which is
   * what zeroes band.alpha and gives the opening its fade-in for free.
   *
   * CALLED TWICE, because reset() is not called at construction; the state
   * literals are the initial state and only camera.reset runs down there.
   */
  const ORDER_HOUSE = letters?.byHouse?.findIndex((h) => h.origin === 'palace') ?? -1;
  const seedOrder = () => { if (ORDER_HOUSE >= 0) filePage({ kind: 'letter', house: ORDER_HOUSE }); };

  /**
   * THE VOICES. Every paper is read aloud by whoever wrote it: Mira reads her
   * letter, the smith his own last words, the palace its order. The band still
   * shows the document; the voice is the same document, word for word, and
   * never a voice that describes one.
   *
   * Named by what they read -- `letter 0` is the forge's letter, `fragment 0`
   * the smith's scrap -- so a voice is found the same way the band's text is.
   */
  const voiceOf = (kind, i) => {
    const h = letters?.byHouse?.[i];
    return (kind === 'letter' ? h?.letterVoice : h?.fragmentVoice) ? `${kind} ${i}` : null;
  };
  letters?.byHouse?.forEach((h, i) => {
    if (h.letterVoice) audio?.useVoice?.(`letter ${i}`, { file: h.letterVoice });
    if (h.fragmentVoice) audio?.useVoice?.(`fragment ${i}`, { file: h.fragmentVoice });
  });
  const ORDERS_VOICE = voiceOf('letter', ORDER_HOUSE);
  // Which document the voice belongs to (the band's key), how long it has been
  // up, and whether its voice has started.
  const voice = { key: null, ms: 0, started: false };

  /**
   * What the band is showing, and it shows the document rather than describing
   * it: the page he is holding up, or the letter on the wall in front of him.
   *
   * `key` says WHICH document, so the band can blank between two of them β€”
   * a page turn, or looking from the door to his papers and back. See the band.
   */
  const band = { text: null, alpha: 0, key: null };

  /**
   * WHAT THIS BUILD IS FOR, counted.
   *
   * The question health is being added to answer is one thing: does he start
   * standing still and looking at something before walking up to it? So the
   * run keeps score of the only number that answers it β€” seconds spent with
   * the torch up, standing still, with nothing hostile near enough to reach
   * him. Watching from safety.
   *
   * Everything else here is context for that one figure.
   */
  const run = { seconds: 0, fights: 0, hitsTaken: 0, deaths: 0, watching: 0, taken: 0 };
  const WATCH_CLEAR_PX = 600;

  // Whether the sheaf is up. A dwell timer used to live here and resolved the
  // oldest unknown thing in his bag; pages replaced it, because what he
  // resolves is the one he turned to.
  const map = { up: false };
  let lastRunFrame = -1;     // so a step fires once per arrival, not once per tick

  const state = {
    prince, torch, camera, background, drained, loss,
    get grace() { return grace; },
    audio,
    ground: clips.ground,
    kept: clips.kept,
    letterArt: clips.letter,
    keyArt: clips.key,
    patchArt,
    socketArt,
    lightning: clips.lightning,
    pageBlank: clips.page_blank,
    sheetClip, mapClip, hurtClip, run, sheaf, band, textFor, letters, page, pageSeen, filedUnder,
    canMark: false,
    markHouse: null,
    canTakeKey: false,
    canPlaceKey: false,
    // Whether there is anything at his feet to pick up. Computed once here
    // rather than re-scanned by main.js to decide what the button says.
    canTake: false,
    seed: SEED,
    houses: HOUSES, houseTint: PALETTE.WATCHERS,
    houseClip, palaceClip, houseArt, doorPoint, keyPoint, letterPoint, slotPoint,
    tree: TREE, treeClip, treeNoteDark, treePoint,
    // What the band is reading off a wall right now, if anything: the house or
    // the tree. Published for the checks, which ask whether it is the same
    // door E would act on.
    readingAt: null,
    // The voice that should be reading right now, if any: `letter 0`,
    // `fragment 2`. Set whether or not sound can play, so the checks can see
    // the decision.
    voice: null,
    // The house E would act on, if any. Same door the band reads.
    nearDoor: null,
    village, gateX: GATE_X, gateStopX: GATE_STOP_X, gateRoomX: GATE_ROOM_X,
    villageGap: VILLAGE_GAP, bowlX0: BOWL.X0,
    flame: { clip: clips.fire, player: createPlayer(clips.fire), frame: 0 },
    torchArm: TORCH_ARM,
    clock: 0,
    gate,
    opening,
    fade: 0,
  };

  /**
   * @param seed  re-deals the village. Checks pass one so a run reproduces;
   *              a player's reset keeps the seed the run started with.
   */
  function reset(seed = null) {
    if (seed !== null) { state.seed = seed >>> 0; dealVillage(state.seed); }
    prince.x = prince.prevX = START_X;
    prince.facing = 1;
    prince.hasSword = true;
    prince.moving = false;
    prince.hits = 0;
    prince.keys = 0;
    prince.run.reset();
    prince.idle.reset();
    torch.reset();
    camera.reset(START_X);
    state.clock = 0;
    for (const h of HOUSES) {
      h.seen = false; h.filed = null;
      h.open = false; h.settled = false; h.keyTaken = false;
    }
    if (TREE) TREE.seen = false;
    still = 0;
    state.readingAt = null;
    state.nearDoor = null;
    audio?.stopVoice?.();
    voice.key = null; voice.ms = 0; voice.started = false;
    state.voice = null;
    prevPickup = false;
    prince.attacking = false;
    prince.attackTime = 0;
    prince.attackConnected = false;
    loss.active = false;
    loss.timer = 0;
    loss.by = null;
    grace = 0;
    sheaf.up = false;
    gate.filled = 0;
    gate.open = 0;
    gate.through = false;
    gate.placed = 0;
    flash.ms = FLASH.MS;
    flash.thundered = true;
    state.flash = 0;
    state.flashMs = -1;
    sheaf.pages.length = 0;
    sheaf.index = 0;
    sheaf.heldDir = 0;
    sheaf.repeat = 0;
    sheaf.turns = 0;
    band.text = null;
    band.alpha = 0;
    band.key = null;
    state.fade = 0;
    // Deaths survive a restart. Everything else about the run does not β€” that
    // is what "the findings are at stake" means.
    run.seconds = 0; run.fights = 0; run.hitsTaken = 0; run.watching = 0; run.taken = 0;
    lastRunFrame = -1;
    prince.inventory.length = 0;
    drained.forEach((d) => d.reset());
    // His orders go back on the pile, because they are the one thing he cannot
    // lose. After the clears above, so they are not what gets cleared.
    seedOrder();
    // AND THE OPENING DOES NOT PLAY AGAIN. reset() is what R calls and what
    // the death beat ends with, and four seconds of re-reading an order you
    // have read is a punishment for dying. Parked finished, which is the same
    // idiom the storm is parked with six lines up.
    opening.done = true;
  }
  seedOrder();
  armOpening();
  camera.reset(START_X);

  function update(dt, input) {
    prince.prevX = prince.x;
    // Where he started this frame, for "is he standing still". Not prevX: a
    // hit and a shove both reset that to stop the draw smearing, so a knock
    // would look like standing still.
    const xFrom = prince.x;
    torch.snapshot();
    camera.snapshot();
    state.clock += dt;

    // --- the opening ------------------------------------------------------
    // He reads his orders, and then the game begins. The real input is looked
    // at exactly once, to see whether to cut it short; after that the frame
    // runs on OPENING_INPUT and he is holding the page up whatever anybody
    // presses. See the note on OPENING_INPUT for why this is not a `return`.
    if (!opening.done) {
      // SKIPPABLE, BY ANY OF THE SIX. A player who has seen it does not have
      // to sit through it, and one who has not will not know to press anything.
      const pressed = input.torch || input.sheaf || input.attack ||
        input.pickup || input.left || input.right;
      let skipped = pressed;
      // BUT THE FIRST PRESS STARTS THE READING. A browser makes no sound before
      // a press, so if the first one also put the orders away nobody would
      // ever hear them read. So while his orders could be read aloud and are
      // not yet, a press asks for the voice and the page stays up; once it is
      // speaking, the NEXT press ends it -- a key still held from the first
      // does not. If the voice has not come VOICE_WAIT_MS after it was asked
      // for, a press ends it as before. With sound off, nothing has changed.
      if (ORDERS_VOICE && audio?.canSpeak?.(ORDERS_VOICE)) {
        if (!opening.heard) {
          if (pressed && opening.askedAt < 0) opening.askedAt = opening.ms;
          skipped = pressed && opening.askedAt >= 0 &&
            opening.ms - opening.askedAt >= TUNING.VOICE_WAIT_MS;
        } else {
          if (!pressed) opening.clear = true;
          skipped = pressed && opening.clear;
        }
      }
      opening.ms += dt * 1000;
      // IT WAITS FOR HIM. The spec asked for a four-second hold that any input
      // could cut short; holding until a key is pressed is the same thing with
      // the impatient case removed, and it lets somebody read at their own
      // speed instead of at mine. ORDER_HOLD_MS is an upper bound for anyone
      // who wants one -- 0, the default, means no bound at all.
      const rangOut = TUNING.ORDER_HOLD_MS > 0 && opening.ms >= TUNING.ORDER_HOLD_MS;
      if (skipped || rangOut) {
        opening.done = true;
        // AND THE PRESS THAT SKIPPED DOES NOT ALSO SWING. attack and pickup
        // are one-shots the SCENE clears, further down; the frames above ran
        // on a different object, so the real one has never been cleared and
        // the key that ended the opening would land as a sword swing on the
        // frame control comes back. The loss beat has the same hole and never
        // has to care, because it ends in reset().
        input.attack = false;
        input.pickup = false;
      } else {
        // A press the opening spent is spent: a swing or a take still set on
        // the real input would count as held for ever, and the next press
        // could never arrive.
        input.attack = false;
        input.pickup = false;
        input = OPENING_INPUT;
      }
    }

    // --- the loss beat ----------------------------------------------------
    // Everything stops. If the torch was up it leaves his hand, falls, rolls
    // forward and goes out; the circle shrinks as it rolls.
    if (loss.active) {
      loss.timer += dt * 1000;
      // He goes down. The clip plays once and holds on the last frame, which is
      // what the light goes out over.
      prince.down.advance(dt);
      prince.clip = downClip;
      prince.frame = prince.down.index;
      // All the way out, over the clip. The last thing on screen before the
      // level starts again is the light leaving him.
      prince.eyeDim = Math.min(1, 0.6 + loss.timer / 900);
      if (loss.torchWasUp) {
        loss.vx += 260 * dt;
        loss.x += loss.vx * dt;
        loss.y = Math.min(GROUND_Y - 20, loss.y + 900 * dt * Math.min(1, loss.timer / 260));
        loss.r = Math.max(0, loss.r - 300 * dt);
        torch.cx = loss.x;
        torch.cy = loss.y;
        torch.radius = loss.r;
      }
      // THE LEVEL RESTARTS. He does not get up where he fell any more.
      //
      // The spec's checkpoint at the mouth keeps the map, and a checkpoint that
      // keeps the findings is too forgiving to change how anybody plays. The
      // findings have to be at stake or the fights are not a cost.
      state.fade = Math.min(1, (loss.timer - TUNING.LOSS_HOLD_MS) / TUNING.DEATH_FADE_MS);
      if (loss.timer >= TUNING.LOSS_HOLD_MS + TUNING.DEATH_FADE_MS) { run.deaths++; reset(); }
      return;
    }

    // --- Torch ------------------------------------------------------------
    // The whole swing is a commitment. No cancelling into the torch, and no
    // cancelling into another swing.
    const committed = prince.attacking;
    // Holding the map raises the torch. One button, and the cost is the torch's
    // cost exactly: he stops, the sword goes away, and he is lit up in the dark.
    sheaf.up = !!input.sheaf && !committed && !!mapClip;
    // FLAME AND AIR, ON THE WAY UP ONLY. Lowering it is him giving up the
    // light, which is a thing that should happen quietly.
    const wantsLight = (!!input.torch || sheaf.up) && !committed;
    litLast = wantsLight;
    torch.update(dt, wantsLight, state.clock,
      prince.x, GROUND_Y, prince.facing, HAND_DX, HAND_DY, TORCH_ARM, torchClip.fireCentre, TORCH_HANDS);
    prince.hasSword = torch.raise <= 0.001;
    // The crackle rides the raise, so it arrives with the light and leaves
    // with it. A loop rather than a one-shot on the press: a torch is not an
    // event, it is a thing that is burning.
    audio?.torchFire?.(torch.raise);

    // THE LIGHT IN HIM, every frame he is still up. Capped short of black so a
    // man on his last hit still has something in his face β€” going fully out is
    // what dying looks like, and it must not be what nearly-dying looks like.
    // SPREAD ACROSS THE HITS, not clamped at the end. Clamping made four hits
    // and five hits the same picture, which loses the one step that matters
    // most β€” the last one before the restart.
    prince.eyeDim = TUNING.EYE_DIM_MAX
      * Math.min(1, prince.hits / Math.max(1, TUNING.PRINCE_HITS - 1));

    // --- Movement ---------------------------------------------------------
    // Torch up means he cannot move. To look, he stops. To move on, he stops
    // looking. Every second spent understanding something is a second not spent
    // leaving.
    if (grace > 0) grace -= dt * 1000;

    const wantDir = (input.right ? 1 : 0) - (input.left ? 1 : 0);
    const canMove = prince.hasSword && !committed;
    prince.moving = canMove && wantDir !== 0;

    if (prince.moving) {
      prince.facing = wantDir;
      prince.x += wantDir * TUNING.WALK_SPEED * dt;
      if (prince.x < 0) prince.x = 0;
      // He does not hit a boundary, he hits a shut gate. Once it is up, the
      // only thing left in his way is the man standing in it.
      //
      // AND THE GATEWAY IS HIS ONCE ITS KEEPER HAS GONE HOME. While the keeper
      // holds the post, the shut leaf stops him just inside the left post.
      // With the post empty he can walk the whole width of the leaf, under all
      // four sockets, to just inside the right one. Found in play: he stood
      // at the left edge and could not get close enough to see how many
      // sockets the gate wanted.
      const shut = keeperGone() ? GATE_ROOM_X : GATE_STOP_X;
      const stop = gate.open >= 1 ? Math.max(GATE_X + 260, shut) : shut;
      if (prince.x > stop) prince.x = stop;
    }
    camera.setBounds(0, keeperGone() ? GATE_ROOM_CAM_X : GATE_CAM_X);
    camera.follow(prince.x);

    // --- The sword --------------------------------------------------------
    if (input.attack) {
      input.attack = false;
      if (prince.hasSword && !prince.attacking) {
        prince.attacking = true;
        prince.attackTime = 0;
        prince.attackConnected = false;
        audio?.swing();
      }
    }
    if (prince.attacking) {
      // THE SWING SLOWS. A cost he feels in the one thing he was using to make
      // the cost not matter.
      const slow = TUNING.SWING_SLOW[Math.min(prince.hits, TUNING.SWING_SLOW.length - 1)];
      prince.attackTime += dt / slow;
      const ms = prince.attackTime * 1000;
      const active = ms >= TUNING.ATTACK_WINDUP_MS &&
                     ms < TUNING.ATTACK_WINDUP_MS + TUNING.ATTACK_ACTIVE_MS;
      // One hitbox, one target, no multi-hit.
      if (active && !prince.attackConnected) {
        for (const d of drained) {
          // THE SWORD SWINGS ALONG THE ROAD. A figure in the back lane is
          // beside it, not on it, so the blade passes in front of it the way it
          // would in any beat 'em up β€” and one halfway through stepping down is
          // not there yet. Without this the lane is a picture: swing at a man
          // standing beside the road and he wakes, which is the exact fight
          // the lane exists to stop anyone starting.
          if (d.lane > 0) continue;
          const gap = (d.x - prince.x) * prince.facing;
          if (gap > -40 && gap <= attackClip.reach) {
            prince.attackConnected = true;
            d.takeHit(prince.x);
            audio?.hit();
            break;
          }
        }
      }
      if (prince.attackTime >= ATTACK_TOTAL) prince.attacking = false;
    }

    // --- the Drained ------------------------------------------------------
    const ctx = { princeX: prince.x, torch, groundY: GROUND_Y, inFrame: false };
    for (const d of drained) {
      // Only what is near enough to matter. Downed bodies persist in world
      // space, so one can rise and come at him from behind.
      //
      // EXCEPT ONE ON ITS WAY HOME. This cull is what makes a village of twelve
      // cost nothing, and it is exactly wrong for the one body that has
      // somewhere to be: skipped here, it never takes a step, so the door it
      // was walking to never opens and the key behind it is lost.
      //
      // ON THE FLAG, NOT THE STATE. Gating on HOMING alone left it culled
      // during the 900ms of GETTING UP, which is a state it then never leaves
      // β€” filing correctly from the far side of the village put a body on its
      // knees for ever. The flag covers the whole journey; it is cleared when
      // the door opens, and the cull takes over again.
      //
      // AND ONE WOKEN BY A WRONG PIN, for the same reason: it is on its way to
      // him, and a body skipped here would sit in the street half risen.
      if (!d.homing && !d.woken && Math.abs(d.x - prince.x) > W * 1.4) continue;
      // On screen together? The camera follows the prince, so this is the same
      // question as "can it see him".
      const sx = d.x - camera.x;
      ctx.inFrame = sx > -260 && sx < W + 260;
      const wasState = d.state;
      const wasFrame = d.frame;
      const wasRevealed = d.revealed;
      const wasLit = d.dwell > 0;
      d.update(dt, ctx);
      // THE LIGHT LANDS AND IT BREATHES IN. Once per look, and not again for a
      // second and a half, or sweeping the torch across it would pant.
      // (A clock earlier than the last breath is a restart, not a short gap.)
      const sinceBreath = state.clock - (d.stirredAt ?? -9);
      if (!wasLit && d.dwell > 0 && !d.revealed && (sinceBreath > 1.5 || sinceBreath < 0)) {
        d.stirredAt = state.clock;
        audio?.drainedStir?.(Math.max(-0.85, Math.min(0.85, (d.x - prince.x) / 1400)));
      }
      if (d.state === DOWNED && wasState !== DOWNED) audio?.bodyFall();
      // ARRIVED. The door is not open because the torch is on it β€” that is
      // LIT, and it has been true since L2. It is open because the person who
      // lived here opened it, and it is the first thing in this level that
      // somebody other than the prince does.
      if (d.state === HOME && wasState !== HOME) {
        const home = HOUSES.find((h) => h.x === d.homeX);
        if (home) home.open = true;
      }
      // One dragged foot when the foot arrives, off the clip's own footFrames,
      // exactly as his boots work. Only drained_walk carries any, so a rush is
      // silent and a body standing in the street is silent.
      //
      // ON SCREEN, OR ON ITS WAY HOME. This used to be on screen only β€” a drag
      // out of an empty frame is a thing that is not there. Except that the
      // one walking home IS there, a few screens off, and played through a
      // whole level nobody could tell it was happening. So that one is heard
      // wherever it is, quieter the further off (AUDIO.HOME_FOOT_FAR), and
      // every drag is placed on the side it is on.
      if (d.frame !== wasFrame && d.clip?.footFrames?.includes(d.frame) &&
          (ctx.inFrame || d.state === HOMING)) {
        const off = d.x - prince.x;
        const beyond = Math.max(0, Math.abs(off) - W / 2);
        const near = Math.max(0, 1 - beyond / Math.max(1, AUDIO.HOME_FOOT_REACH));
        const level = ctx.inFrame ? 1 : AUDIO.HOME_FOOT_FAR + (1 - AUDIO.HOME_FOOT_FAR) * near;
        audio?.drainedStep(level, Math.max(-0.85, Math.min(0.85, off / 1400)));
      }
      // AND ONE VOICE PER KIND. The clip's own name says which β€” a swing, a
      // haul, a bearing, one coming for him, the gatekeeper winding up β€” and
      // the clip's own soundFrames say when. Nothing here knows what any of
      // them sounds like, which is the same deal footFrames already has.
      //
      // They are BODY sounds and never tool sounds, and audio.js says at
      // length why: a hammer-on-anvil clang would send every player to the
      // swinging shape, and the forge's fragment is deliberately on a hauler.
      //
      // AND ONE COMING FOR HIM IS HEARD COMING. Woken by a wrong pin it rushes
      // from wherever it fell, so its breathing plays off screen too: faint far
      // off, louder as it closes, on the side it is on. The walk home's drag
      // does the same, off the same numbers.
      if (d.frame !== wasFrame && d.clip?.soundFrames?.includes(d.frame) &&
          (ctx.inFrame || (d.woken && d.state === RUSH))) {
        const off = d.x - prince.x;
        const near = Math.max(0, 1 - Math.max(0, Math.abs(off) - W / 2) / Math.max(1, AUDIO.HOME_FOOT_REACH));
        audio?.drainedVoice?.(d.clip.name,
          ctx.inFrame ? 1 : AUDIO.HOME_FOOT_FAR + (1 - AUDIO.HOME_FOOT_FAR) * near,
          Math.max(-0.85, Math.min(0.85, off / 1400)));
      }
      // THE INSTANT IT NOTICES HIM. `revealed` latches and never unlatches, so
      // this fires once per shape per run β€” about twelve times, and every one
      // of them is a fight he chose by deciding to see.
      if (d.revealed && !wasRevealed) audio?.drainedWake?.();
      const kind = grace > 0 ? null : d.contactKind(prince.x, CONTACT_REACH);
      // Contact with a WANDER one is not a loss. He walks into someone standing
      // in the dark and nothing happens at all, which is worse.
      if (kind === 'shove' && grace <= 0) {
        // He loses ground AND one of his six. He was the only thing in the
        // village that could not hurt you, which made the hard fight free.
        loss.by = d;
        const back = d.x + Math.sign(prince.x - d.x || -1) * (TUNING.POST_REACH + TUNING.POST_SHOVE_PX);
        takeHit(d.x, 0);
        if (!loss.active) { prince.x = back; prince.prevX = back; }
      }
      if (kind === 'fatal' && !loss.active) {
        loss.by = d;
        takeHit(d.x, TUNING.HURT_KNOCK_PX);
      }
    }

    // --- reading it ---------------------------------------------------------
    // HE CAN LIGHT WHAT HE IS CARRYING, and this closes a hole rather than
    // adding a convenience. The gate wants four objects he has LOOKED at; take
    // four blind and the spec sent him back "to light four things on the
    // ground", except he took them, so there is nothing on the ground and the
    // gate could never open.
    //
    // At L3 that is no longer a special case at all β€” a fragment he took is a
    // page in the sheaf like any other, and paging to it is the looking.
    // --- the sheaf ----------------------------------------------------------
    //
    // PAGING. Left and right while the sheaf is up. He cannot walk with it
    // raised, so those keys are free β€” and on touch the walk arrows do the same
    // job, which is what a person does with a handful of letters: the pile
    // under one thumb and the turning under the other.
    //
    // A FLIP WITH A BEAT. The turn fires on the press and does not fire again
    // until SHEAF_REPEAT_MS, so holding an arrow walks the pile at reading
    // speed. Free-spinning it would make the sheaf a menu.
    //
    // IT DOES NOT WRAP. Running off the end of a pile of paper does nothing,
    // and wrapping would put the map next to the last fragment for no reason a
    // player could see.
    if (sheaf.up) {
      const dir = (input.right ? 1 : 0) - (input.left ? 1 : 0);
      if (dir === 0) { sheaf.heldDir = 0; sheaf.repeat = 0; }
      else {
        const fresh = dir !== sheaf.heldDir;
        if (fresh) { sheaf.heldDir = dir; sheaf.repeat = 0; }
        else sheaf.repeat -= dt * 1000;
        if (fresh || sheaf.repeat <= 0) {
          sheaf.repeat = SHEAF_REPEAT_MS;
          const next = Math.min(sheaf.pages.length - 1, Math.max(0, sheaf.index + dir));
          if (next !== sheaf.index) {
            sheaf.index = next;
            sheaf.turns++;
            audio?.pageTurn?.();
          }
        }
      }
    } else {
      // THE INDEX SURVIVES RELEASE. He reopens where he left off; resetting to
      // the map would make every glance start from page one, and browsing a
      // pile you have to walk back through is a chore rather than a document.
      sheaf.heldDir = 0;
      sheaf.repeat = 0;
    }

    // A `?` RESOLVES THE MOMENT HE PAGES TO IT, and there is no timer on it.
    //
    // Holding the sheaf raises the torch, so a page he is on is already being
    // held in his own light β€” the looking has happened, and charging him
    // another four hundred milliseconds for it would only mean that skimming
    // does not read, which is a rule nobody would ever discover.
    //
    // THE COST IS STILL REAL AND IT IS STILL THE SAME. The beat on a page turn
    // is 260ms, so walking four unknown pages is about a second stood still in
    // the open with the torch up β€” within a whisker of what the old timer
    // charged. What changed is that he resolves the one he turned to rather
    // than the oldest thing in his bag.
    //
    // This is why there is no visible unknown state on the sheet for a page he
    // has opened. The unknown drawing is for the pages BEHIND the one he is on:
    // take three blind and only the last is in his hand.
    if (sheaf.up && torch.raise > 0.99) {
      const pg = page();
      if (!pageSeen(pg)) {
        const it = prince.inventory.find((o) => o.kept === pg.kept);
        if (it) { it.seen = true; sheaf.turns++; }
      }
    }

    // --- the doors ----------------------------------------------------------
    // Lighting one is the only thing that says who lived here. It latches, the
    // same way seeing an object on the ground latches, and for the same reason:
    // he looked, and looking is not something he can un-do.
    //
    // THE DOOR OR ITS LETTER, EITHER ONE. The letter hangs left of the arch,
    // nearer him than the door, so there is a stretch of wall where the light
    // is on the letter and not yet on the door. If only the door counted he
    // could read a letter in the band and then find the door had never been
    // lit: the next read would not come free, and E would not pin.
    for (const h of HOUSES) {
      const [dx, dy] = doorPoint(h);
      const lp = letterPoint(h);
      if (torch.covers(dx, dy) || (lp && torch.covers(lp[0], lp[1]))) h.seen = true;
    }
    // THE NOTE ON THE TREE. No door to light first: the paper is all there is.
    if (TREE) {
      const p = treePoint();
      if (p && torch.covers(p[0], p[1])) TREE.seen = true;
    }

    // THE DOOR HE IS AT, AND IT IS LATCHED RATHER THAN LIT.
    //
    // There used to be a `lit` house here, meaning "the torch is on this door
    // right now", and it was what marking tested β€” which only worked because
    // holding the sheaf forced the torch up. Seeing latches instead, and acting
    // on it only needs him to still be standing there.
    //
    // ONE DOOR, AND THE BAND AND E BOTH USE IT. The band never reads him one
    // door while E acts on another. The doors are about 4200px apart and the
    // reach is 440, so there is never more than one to choose from anyway.
    let nearDoor = null;
    for (const h of HOUSES) {
      if (!h.seen) continue;
      const p = slotPoint(h);
      if (p && Math.abs(prince.x - p[0]) <= TUNING.DOOR_REACH) { nearDoor = h; break; }
    }
    // And the tree, on the same terms. It reads like a door and it never joins
    // nearDoor: there is nothing on it to pin to.
    let nearTree = null;
    if (TREE?.seen) {
      const p = treePoint();
      if (p && Math.abs(prince.x - p[0]) <= TUNING.DOOR_REACH) nearTree = TREE;
    }

    // AND THE KEY, WHICH NEEDS THE DOOR OPEN AND NOT THE TORCH UP.
    //
    // Two door states and this is the line between them. `seen` is what the
    // torch buys: you can look into the room and the key is hanging there in
    // the light. `open` is what the person buys by walking home: you can reach
    // in. It is visible from the first time the door is lit and it is not his
    // until somebody comes back and opens their own door, and nothing has to
    // say so β€” the halo is the game's word for "you can take this" and it
    // simply is not on yet.
    let litKey = null;
    for (const h of HOUSES) {
      if (!h.open || h.keyTaken) continue;
      const p = keyPoint(h);
      if (p && Math.abs(prince.x - p[0]) <= TUNING.DOOR_REACH) { litKey = h; break; }
    }

    // --- the band -----------------------------------------------------------
    //
    // IT SHOWS A DOCUMENT, VERBATIM, and a document is in one of two places: in
    // his hand, or nailed to the wall in front of him. Nothing else in this
    // game gets text: not doors, not the gate, not objects and not the prince.
    //
    //   Q held                                   the page in his hand. Wins
    //   the torch on a letter, in reach          that letter. The first read
    //   standing still, in reach of a lit door   that door's letter
    //   otherwise                                closed
    //
    // COMPARING IS LOOKING AT THE DOOR, THEN AT HIS PAPERS, THEN AT THE DOOR.
    // Hold Q and the band reads the fragment in his hand; let go and it reads
    // the door again. That is the whole deduction, stood in one place.
    //
    // THE FIRST READ NEEDS LIGHT, AND ONLY THE FIRST. The first draft had every
    // read need the torch, so comparing was SPACE held with Q tapped in
    // between. On a phone TORCH and PAGES are both on the left wheel and one
    // thumb cannot hold one while pressing the other β€” the trap taking a letter
    // fell into once already. So a door he has lit reads again for nothing,
    // and comparing is one button: PAGES down, PAGES up.
    //
    // STANDING STILL is what stops it opening at every door he walks past. Not
    // walking, not swinging and not knocked back, for READ_STILL_MS. Holding Q
    // counts β€” he cannot walk with it up β€” so letting go of Q goes straight
    // back to the door.
    const stood = !prince.moving && !prince.attacking && prince.x === xFrom;
    still = stood ? still + dt * 1000 : 0;
    const readAt = nearDoor ?? nearTree;
    const readPoint = readAt === TREE ? treePoint() : readAt ? letterPoint(readAt) : null;
    const reading = !!readAt &&
      ((!!readPoint && torch.covers(readPoint[0], readPoint[1])) ||
        still >= TUNING.READ_STILL_MS);
    state.readingAt = sheaf.up || !reading ? null : readAt;
    // Gated on the raise so the band and the page arrive together β€” the sheet
    // itself is not drawn until the arm is up, and a band that beat it there
    // would read as a subtitle rather than as the paper in his hand.
    //
    // THE SHEAF WINS FROM THE MOMENT Q GOES DOWN, not from when the page is
    // up. For the quarter-second of the raise the band is blank: he has looked
    // away from the wall and not yet down at his hand.
    const pg = sheaf.up && torch.raise > 0.9 ? page() : null;
    // AN UNKNOWN PAGE HAS NOTHING ON IT YET. He took it blind and he has not
    // looked; the band would be reading him words he cannot have.
    const want = sheaf.up ? (pg && pageSeen(pg) ? textFor(pg) : null)
      : state.readingAt ? textFor({ kind: 'letter', house: state.readingAt.index })
        : null;

    // CLEARED AND REFILLED EVERY TIME IT CHANGES DOCUMENT, never swapped in
    // place. The blank between two pages is a fraction of a second and it is
    // the entire reason a turn reads as a turn β€” swap the text and two letters
    // in a row look like one letter changing its mind. Looking from the door to
    // the page in his hand is the same thing: two pieces of paper, not one
    // label flipping.
    //
    // A page is keyed by the turn count rather than by its words, because
    // paging from one page to another with the SAME words has to still read as
    // a turn.
    const key = !want ? null : pg ? `page ${sheaf.turns}` : `wall ${state.readingAt.index}`;
    if (key !== band.key) { band.key = key; if (key) band.alpha = 0; }
    band.text = want;
    band.alpha = Math.min(1, Math.max(0, band.alpha + (want ? dt * 5 : -dt * 9)));

    // --- the voice ----------------------------------------------------------
    //
    // THE PAPER, READ ALOUD, while the band shows it. It follows the band's key
    // exactly: a new document stops the old voice, and looking back at one
    // reads it again from the top, the way reading it again does.
    //
    // NOT UNTIL IT HAS BEEN UP VOICE_DELAY_MS, which is longer than a page
    // turn's beat -- so paging through the pile with an arrow held flips past
    // every page in silence, and only the one he stops on speaks. A page he has
    // not looked at has nothing written on it, so it has nothing to say either.
    const said = !want ? null
      : pg ? (pg.kind === 'fragment' ? voiceOf('fragment', pg.kept)
        : pg.kind === 'letter' ? voiceOf('letter', pg.house) : null)
        : voiceOf('letter', state.readingAt.index);
    if (key !== voice.key) {
      if (voice.started) audio?.stopVoice?.();
      voice.key = key;
      voice.ms = 0;
      voice.started = false;
    } else {
      voice.ms += dt * 1000;
    }
    // AND NOT WHILE HE DIES. The hit that kills him lands earlier in this same
    // frame and stops the voice; without this, the band -- still showing the
    // same page -- read it again from the top over his death.
    state.voice = said && voice.ms >= TUNING.VOICE_DELAY_MS && !loss.active ? said : null;
    // Asked every frame until it answers: before the first press, or before
    // the file has decoded, it cannot.
    if (state.voice && !voice.started) voice.started = !!audio?.voice?.(state.voice);
    if (voice.started && !opening.done && state.voice === ORDERS_VOICE) opening.heard = true;

    // --- marking the map ----------------------------------------------------
    //
    // HE WRITES IT. THE GAME DOES NOT. Nothing on the map fills itself any
    // more: what goes under a plot is a claim he makes, and a claim only means
    // something if it can be wrong.
    //
    // THE ACT IS THE SAME POSTURE AS EVERYTHING ELSE. He stands at a door with
    // a piece of paper in his hand and presses take. No menu, no drag, no
    // assignment screen β€” and no new button, because TAKE is already the
    // contextual one and pinning is "put this where it goes".
    //
    // The door has to have been LIT ONCE β€” the same latch that reads it again
    // for free β€” and he has to be standing at it. Not lit now and not with Q
    // held: asking for both hands is the problem the hand addendum fixed.
    // ONE ACT PER PRESS, DECIDED HERE AND NOT UPSTREAM.
    //
    // input.js already makes `pickup` a one-shot, and that was enough while
    // taking was idempotent β€” pressing take at a door you had already filed
    // simply filed it again. Now that the same button also takes a page back
    // OUT of a frame, a flag that stays true is a toggle running at 60Hz, and
    // whether it lands filed or empty depends on whether the number of frames
    // was odd.
    //
    // The checks are what make that real rather than theoretical: they drive
    // these flags directly and hold as many as they like for as long as they
    // like, which is the same blind spot that let two impossible phone
    // controls pass every test in the suite. So the edge is found here, where
    // the act is, and the scene stops depending on how the flag was made.
    const pickupHeld = !!input.pickup;
    const wantsPickup = pickupHeld && !prevPickup;
    prevPickup = pickupHeld;
    input.pickup = false;
    // --- what is in his hand ------------------------------------------------
    //
    // THE TOP PAGE OF THE PILE IS WHAT HE IS HOLDING, and the pile does not
    // have to be up for him to be holding it.
    //
    // It used to. Pinning a fragment meant holding Q, paging to it, and
    // pressing E at a door β€” three conditions and a mode, and it was played and
    // nobody did it. Nobody does that with paper either: you take the page you
    // want, put the rest away, walk over, and pin it.
    //
    // So the sheaf is for CHOOSING and E is for DOING, and E never asks about
    // the sheaf. Q still raises the torch, still stops him, still shows the
    // band and pages with the arrows. It has just stopped being a prerequisite.
    const handPage = page();
    state.handPage = handPage;

    // THE SLOT TAKES A PIN FROM THE FIRST LIGHT. It used to wait for the letter
    // to come off: the patch was earned, and taking the letter was what showed
    // him where a fragment goes. The letter never comes off now, and the slot
    // is beside it from the start β€” a bare nail under a letter says something
    // goes here.
    //
    // AND NOT AT A DOOR THAT IS FINISHED WITH. Someone walked home to it; the
    // slot has the only thing it was ever going to hold.
    const patchTakes = !!nearDoor && !gate.through && !nearDoor.settled;
    const holding = handPage?.kind === 'fragment' ? handPage.kept : null;

    // PIN, or TAKE BACK, and which one is decided by what is already there.
    //
    // THE SAME PAGE THAT IS ALREADY PINNED TAKES IT BACK. The table this comes
    // from does not have a row for it β€” it has "a different fragment evicts and
    // pins" and "not a fragment takes it back" β€” and holding the page that is
    // already on the wall is neither. Evicting it and pinning it again is a
    // press that does nothing, so it is the other one: he is holding the thing
    // in front of him, and E takes it down.
    const canRetrieve = patchTakes && nearDoor.filed != null &&
      (holding === null || holding === nearDoor.filed);
    const canPin = patchTakes && holding !== null && holding !== nearDoor.filed;
    const canMark = canPin || canRetrieve;

    // THE SLOT, THEN THE KEY. A claim about a door comes before the thing that
    // opens the gate: evidence before conclusions before rewards, which is the
    // order this whole game is arguing for.
    //
    // READING IS NOT ON THIS LIST. What the band shows decides nothing about E.
    // It used to β€” a letter in reach switched off pinning until it was taken β€”
    // and with letters never taken that would have switched pinning off at
    // exactly the moment he reads the letter he is trying to answer.
    state.canMark = canMark;
    state.canPin = canPin;
    state.nearDoor = nearDoor;
    // WHICH house it would go under. The sheet draws that house's plot beside
    // the page when this is set, so the paper itself says "this one, here".
    state.markHouse = state.canPin ? nearDoor.index : null;
    state.canTakeKey = !canMark && !!litKey;
    // AND PUTTING ONE IN. He stops 380px short of the leaf and can never get
    // closer, so the reach is the door's, not the ground's.
    state.canPlaceKey = !canMark && prince.keys > 0 && !gate.through &&
      gate.placed < TUNING.GATE_WANTS &&
      Math.abs(prince.x - GATE_X) <= TUNING.DOOR_REACH;
    if (wantsPickup && state.canMark && !prince.attacking) {
      if (canRetrieve) {
        // TAKE IT BACK DOWN. Either he is holding the page that is already
        // pinned here, or he is holding nothing that could go in β€” and in both
        // cases the only thing on that wall he can act on is what is on it.
        //
        // It never left the sheaf. The patch holds a CLAIM, not the paper, so
        // there is nothing to hand back.
        nearDoor.filed = null;
        audio?.mark?.();
      } else {
        // MOVE, THEN REPLACE. Pinning a fragment somewhere else takes it off
        // wherever it was, and pinning a different one here pushes the old one
        // out. Two fragments can never sit in one patch and one fragment can
        // never sit in two, and neither rule needs a message: it is what
        // putting a piece of paper down on a table already does.
        for (const h of HOUSES) if (h.filed === holding) h.filed = null;
        nearDoor.filed = holding;
        audio?.mark?.();

        // AND IF IT WAS THEIRS, THEY GO HOME.
        //
        // `h.index` and `HOUSE_OF[i]` have been the same number since the
        // fragments were written, so this is the whole correctness test and
        // there is nowhere for it to drift to.
        //
        // NOTHING IS SAID. No tick, no sound of being right, no change to the
        // patch. Somewhere out in the village a body gets up, and the only way
        // to find out is that it walks up the street.
        if (nearDoor.index === holding) {
          const owner = drained.find((d) => d.kept === holding && d.keptTaken);
          if (owner) {
            nearDoor.settled = true;
            owner.sendHome(nearDoor.x);
            // AND THE SKY GOES OFF. It replaces a silence: the payoff for the
            // one right act in this level happens somewhere else and up to a
            // minute later, so doing it correctly looked exactly like doing
            // nothing. A ten-year-old played it and stopped here.
            flash.ms = 0;
            flash.thundered = false;
          }
        } else {
          // AND IF IT WAS NOT, THEY OBJECT. Played through, the level was
          // finished without reading a word: pin, see who walks, try the next
          // door. The walk home confirmed every guess instantly and for free,
          // which made it an answer key. Now a stranger's house wakes the
          // owner, and it comes for him. Guessing is still allowed; it costs.
          //
          // The page stays on the wrong wall. Putting it down does not take it
          // back β€” he does that, with E, as always.
          const owner = drained.find((d) => d.kept === holding && d.keptTaken);
          if (owner && owner.wake()) {
            // HEARD AT FULL STRENGTH, WHEREVER IT IS. On the side it is on, but
            // never quieter for being far: a faint gasp from across the village
            // and a rush thirty seconds later are two things nobody connects.
            // The breathing as it comes can fall away with distance. The
            // moment it wakes cannot.
            audio?.drainedWake?.(1, Math.max(-0.85, Math.min(0.85, (owner.x - prince.x) / 1400)));
          }
        }
      }
    }

    // --- the objects on the ground -----------------------------------------
    // Seeing one is latched separately from picking it up, because they are
    // different acts with different costs. The glow tells you something is
    // there without the torch; only the torch tells you what.
    let nearest = null;
    let nearestGap = TUNING.PICKUP_REACH;
    for (const d of drained) {
      if (!d.keptDropped || d.keptTaken) continue;
      if (torch.covers(d.keptX, GROUND_Y + d.keptY)) d.keptSeen = true;
      const gap = Math.abs(d.keptX - prince.x);
      if (gap <= nearestGap) { nearest = d; nearestGap = gap; }
    }
    // Published rather than re-derived. main.js used to run this same scan a
    // second time to decide whether to light the button, which is two places
    // that have to agree about what is in reach.
    state.canTake = !!nearest;
    if (wantsPickup && state.canPlaceKey && !prince.attacking) {
      // THE SAME POSTURE AS EVERYTHING ELSE. He stands at the thing and
      // presses take. Four times, one socket each, and the leaf lifts on the
      // fourth β€” no ceremony, no confirmation, and nothing said about the
      // fifth house whose key is still hanging behind a door that never opened.
      prince.keys--;
      gate.placed++;
      // Out of the inventory too, or he is holding a key that is in the gate.
      // The last one found, so the pile behaves like a pile.
      for (let i = sheaf.pages.length - 1; i >= 0; i--) {
        if (sheaf.pages[i].kind === 'key') { sheaf.pages.splice(i, 1); break; }
      }
      sheaf.index = Math.max(0, Math.min(sheaf.index, sheaf.pages.length - 1));
      audio?.mark?.();
    } else if (wantsPickup && state.canTakeKey && !prince.attacking) {
      // Off the hook and into his pocket. No page, no band, no entry in the
      // sheaf: there is nothing to read on a key and nothing to work out.
      litKey.keyTaken = true;
      prince.keys++;
      // AND IT GOES IN WITH EVERYTHING ELSE HE IS CARRYING.
      //
      // Keys were a number and nothing else β€” deliberately, because "a key is
      // not evidence, it does not go in the sheaf, there is nothing to work out
      // about one". All true, and it made them invisible: somebody collected
      // four and did not know until they put one in the gate by accident. A
      // thing you carry that you cannot look at is a thing you do not have.
      //
      // Nothing to read on it, so the band stays empty on a key page. That is
      // the difference between a key and a letter, said by showing you both.
      filePage({ kind: 'key' });
      audio?.keyTake?.();
    } else if (wantsPickup && !state.canMark && nearest && !prince.attacking) {
      nearest.keptTaken = true;
      run.taken++;
      prince.inventory.push({
        kept: nearest.kept, answers: nearest.answers, seen: nearest.keptSeen,
      });
      // Into the sheaf where he found it, looked at or not. A blind take gets a
      // page with nothing written on it yet, and paging to it is what writes it.
      filePage({ kind: 'fragment', kept: nearest.kept });
    }

    // --- the score ----------------------------------------------------------
    run.seconds += dt;
    run.fights = drained.filter((d) => d.revealed).length;
    if (torch.raise > 0.9 && !prince.moving) {
      const near = drained.some((d) => d.state !== DOWNED &&
        Math.abs(d.x - prince.x) < WATCH_CLEAR_PX);
      if (!near) run.watching += dt;
    }

    // --- the storm ----------------------------------------------------------
    // Light first, sound after, because that is the order they arrive in.
    if (!flash.thundered || flash.ms < FLASH.MS) {
      const was = flash.ms;
      flash.ms += dt * 1000;
      if (!flash.thundered && was < FLASH.THUNDER_MS && flash.ms >= FLASH.THUNDER_MS) {
        flash.thundered = true;
        audio?.thunder?.();
      }
    }
    // THE VILLAGE, UNDER EVERYTHING. Up over the first stretch of the bowl and
    // down again at the gate, so walking in is walking into it and walking out
    // is not β€” the same shape the canopy already has, for the same reason.
    //
    // Forty-five people are standing in this place sixty years after she came
    // and until now the only sound in it was his own boots.
    {
      const u = (prince.x - BOWL.X0) / 1800;
      const out = (GATE_X - prince.x) / 1400;
      audio?.villageBed?.(Math.max(0, Math.min(1, u, out)));
    }
    state.flash = flashAt(flash.ms);
    // The bolt runs off the same clock. -1 while nothing is happening, so the
    // renderer does not have to know what "no strike" looks like.
    state.flashMs = flash.thundered && flash.ms >= FLASH.MS ? -1 : flash.ms;

    // --- the gate -----------------------------------------------------------
    //
    // FOUR KEYS IN FOUR SOCKETS. The sockets were built at L1 and have had
    // nothing to go in them since.
    //
    // WHAT THIS REPLACES, and it is deleted rather than turned off: the gate
    // used to want four fragments he had LOOKED at. That was a magic lock β€”
    // the only person who could have known what the marks meant is lying in
    // front of it β€” and it was also the whole reason the level could be beaten
    // without reading a word, because looking at four pieces of paper is not
    // the same act as understanding one.
    //
    // A key is not a grade either. The gate still never sees the map, still
    // never checks an answer, and a player can put four keys in it holding a
    // completely wrong sheaf. What changed is where a key comes from: a door
    // somebody opened from the inside, because you gave them their name back.
    gate.filled = Math.min(TUNING.GATE_WANTS, gate.placed);
    if (gate.filled >= TUNING.GATE_WANTS && gate.open < 1) {
      gate.open = Math.min(1, gate.open + (dt * 1000) / TUNING.GATE_OPEN_MS);
    }
    // THROUGH IS A STEP, not a place. He can be standing inside the gateway
    // when the leaf lifts β€” it is his to walk once the keeper is home β€” and
    // the level must not end on him for standing still under it.
    if (gate.open >= 1 && prince.x >= GATE_X - 40 && prince.moving && prince.facing > 0) {
      // Through it, and the playthrough is over. No prompt, no score screen,
      // and no mention of the empty slot. The gate wanted four. It got four.
      gate.through = true;
    }
    if (gate.through) state.fade = Math.min(1, state.fade + dt / 1.6);

    // --- Prince animation --------------------------------------------------
    if (prince.attacking) {
      prince.clip = attackClip;
      prince.frame = Math.min(Math.floor(prince.attackTime * attackClip.fps), attackClip.count - 1);
    } else if (!prince.hasSword) {
      const eased = torch.raise * torch.raise * (3 - 2 * torch.raise);
      // Same raise curve either way; only which pose it drives changes.
      const clip = sheaf.up && mapClip ? mapClip : torchClip;
      const player = sheaf.up && mapClip ? prince.mapPlayer : prince.torchPlayer;
      player.time = (eased * (clip.count - 1)) / clip.fps;
      prince.clip = clip;
      prince.frame = player.index;
      state.flame.player.advance(dt);
      state.flame.frame = state.flame.player.index;
    } else if (prince.moving) {
      const rate = runClip.groundSpeed ? TUNING.WALK_SPEED / runClip.groundSpeed : 1;
      // THE LIMP, AND IT IS HER OWN RUN FRAMES.
      //
      // A limp is an asymmetric rhythm, not a different drawing: the good foot
      // carries longer and the bad one is hurried off. So the cycle is slowed
      // through its second half and rushed through the first, by an amount that
      // grows with the hits he has taken. Nothing was redrawn for it, and it
      // reads because it is what a limp actually is.
      const hurt = Math.max(0, prince.hits - TUNING.LIMP_FROM);
      if (hurt > 0) {
        const half = prince.run.index >= runClip.count / 2;
        const hitch = 1 - Math.min(0.8, TUNING.LIMP_HITCH * (hurt / 3)) * (half ? 1 : -0.5);
        prince.run.advance(dt * rate / hitch);
      } else {
        prince.run.advance(dt * rate);
      }
      prince.clip = runClip;
      prince.frame = prince.run.index;
      // A footstep when the boot ARRIVES, not on a timer. runClip.footFrames is
      // measured off the drawings, so if the art changes the steps follow it.
      if (prince.frame !== lastRunFrame) {
        if (runClip.footFrames?.includes(prince.frame)) audio?.footstep();
        lastRunFrame = prince.frame;
      }
    } else if (prince.hits >= TUNING.LIMP_FROM && hurtClip) {
      // Standing still with something left in him, he holds his side.
      //
      // NOT PART OF THE READOUT ANY MORE, and not currently drawn. The eye
      // dimming says how hurt he is now; this said it with his body, which is
      // better, but only with a real drawing behind it.
      //
      // Two stages. Which pair of frames he is in is how close he is β€” the
      // first is hurt, the second is spent, and the switch is at the halfway
      // point of what he can take.
      prince.hurtPlayer.advance(dt);
      const spent = prince.hits >= Math.ceil(TUNING.PRINCE_HITS / 2) + 1;
      prince.clip = hurtClip;
      prince.frame = (spent ? 2 : 0) + (prince.hurtPlayer.index % 2);
    } else if (drained.some((d) => d.revealed && d.state !== DOWNED && Math.abs(d.x - prince.x) < W)) {
      // Once one has been seen and is coming, he stands ready.
      prince.guard.advance(dt);
      prince.clip = guardClip;
      prince.frame = prince.guard.index;
    } else {
      prince.idle.advance(dt);
      prince.clip = idleClip;
      prince.frame = prince.idle.index;
    }
  }

  // openAgain is for the checks, and for nothing else in the game: reset()
  // deliberately does NOT re-arm the opening, so something has to be able to.
  // An explicit call beats a check reaching in and setting two fields.
  return { state, update, reset, camera, openAgain: armOpening };
}