Commit de2460845e

de2460845efda51820eb3c6343fb158661ae76bb

parent: f1fdbe26c8

Unregistered key

cmc <hello@cleberg.net> · 2026-07-12 23:33 UTC
committer: <noreply@github.com>

Add 1MB roguelike, player README, and 1KB polish (#1)

* commit WIP 1mb version

* Fix beast-collision exploit and shadow-twist torch bug; add balance constants and mobile D-pad

- Exit tiles no longer bypass the beast-adjacency death check
- Shadow-twist turns now consume torch fuel like a normal wait
- Extract torch/beast/relic magic numbers into named constants
- Fix floor 2 beast enrage threshold (operator precedence bug)
- Add viewport meta tag and on-screen D-pad for mobile play

* Split game data into data.js for mod support

* Add poisoned effigy hazard: decoy relics that kill on touch

* Add minimalist Web Audio sound effects driven by existing fx events

* Add camp-checkpoint save/continue via localStorage

* Add optional floor 7 postgame descent, unlocked after winning

* Add three character-class starting kits (heavy torch, swift, warded)

* Check off completed roadmap items in BLUEPRINT.md

* Fix relic spots colliding with higher-priority tiles across all floors

RELIC_SPOTS coordinates on every floor overlapped exactly with
BLOCK_SPOTS, SHRINE_SPOTS, or BRAZIER_SPOTS entries, and tile() resolves
those categories before relics, so the relic silently became
unreachable whenever the colliding spot was chosen. Floor 6 was the
worst case: its relic and brazier candidate lists were identical,
masking every relic every run. Moved each affected relic to a free,
verified-floor coordinate in the same room instead.

* Add player-facing README, drop dev blueprint, bump 1KB font size

Replace 1mb/BLUEPRINT.md (a dev-oriented architecture doc) with a
top-level README aimed at players, covering both games' controls and
goals with links to the hosted versions. Also increase the 1KB
version's font size for readability, trimming the now-redundant
favicon-suppression tag to stay within the strict 1,024-byte budget.

Layout: unified · split

1kb/index.html +1 −1
@@ -1 +1 @@
1<!doctype html><link rel=icon href=data:,><style>body>div{display:none}body>div:target{display:block}</style><div id=g><p>gate<p>ruin.dark ahead.<p><a href=#b>left</a> <a href=#e>right</a> <a href=#h>in</a></div><div id=b><p>bones<p>torch burns.dark hugs walls.<p><a href=#h>take</a> <a href=#p>on</a></div><div id=e><p>beast<p>one eye opens.<a href=#d>eaten</a></div><div id=h><p>hall<p>stairs.bronze door.wall crack.dark arch.<p><a href=#p>stairs</a> <a href=#i>door</a> <a href=#c>crack</a> <a href=#d>arch</a></div><div id=p><p>pit<p>too wide.dark below.<p><a href=#d>jump</a> <a href=#v>edge</a></div><div id=i><p>idol<p>gold below.head bowed.<p><a href=#d>gold</a> <a href=#c>bow</a> <a href=#h>back</a></div><div id=c><p>crack<p>cold air.fits.<p><a href=#v>squeeze</a> <a href=#h>return</a></div><div id=v><p>vault<p>crown.small key.<p><a href=#d>crown</a> <a href=#w>key</a> <a href=#g>leave</a></div><div id=d><p>dead.<p><a href=#g>again</a></div><div id=w><p>key.<p>gate opens.dawn.<p><a href=#g>again</a></div>
1<!doctype html><style>body>div{display:none}body>div:target{display:block;font-size:1.5em}</style><div id=g><p>gate<p>ruin.dark ahead.<p><a href=#b>left</a> <a href=#e>right</a> <a href=#h>in</a></div><div id=b><p>bones<p>torch burns.dark hugs walls.<p><a href=#h>take</a> <a href=#p>on</a></div><div id=e><p>beast<p>one eye opens.<a href=#d>eaten</a></div><div id=h><p>hall<p>stairs.bronze door.wall crack.dark arch.<p><a href=#p>stairs</a> <a href=#i>door</a> <a href=#c>crack</a> <a href=#d>arch</a></div><div id=p><p>pit<p>too wide.dark below.<p><a href=#d>jump</a> <a href=#v>edge</a></div><div id=i><p>idol<p>gold below.head bowed.<p><a href=#d>gold</a> <a href=#c>bow</a> <a href=#h>back</a></div><div id=c><p>crack<p>cold air.fits.<p><a href=#v>squeeze</a> <a href=#h>return</a></div><div id=v><p>vault<p>crown.small key.<p><a href=#d>crown</a> <a href=#w>key</a> <a href=#g>leave</a></div><div id=d><p>dead.<p><a href=#g>again</a></div><div id=w><p>key.<p>gate opens.dawn.<p><a href=#g>again</a></div>
1mb/data.js added +1255
@@ -0,0 +1,1255 @@
1// Game data — edit this file to mod the run (floors, relics, hazards, balance).
2// Loaded before the engine in index.html.
3const W=12,H=12,FUEL=16
4const BEAST_CALM_RADIUS=4,BEAST_ENRAGED_SPEED=2,TORCH_DRAIN_BASE=1,TORCH_DRAIN_HAZARD=2,RELIC_SLOTS_LOW=2,RELIC_SLOTS_MID=3,RELIC_SLOTS_HIGH=4,RELIC_SLOTS_RICH_BONUS=1
5const P={g:[5,2],b:[2,2],e:[2,9],h:[2,9],p:[2,2],i:[2,2],c:[2,2],v:[2,2],d:[2,2]}
6const PITS=[[
7"############",
8"#OOOOOOOOOO#",
9"#O....F...O#",
10"#O...OO...O#",
11"#O........O#",
12"#O....>...O#",
13"#O........O#",
14"#O...OO...O#",
15"#O........O#",
16"#O........O#",
17"#OOOOOOOOOO#",
18"############"],[
19"############",
20"#OOOOOOOOOO#",
21"#O.......OO#",
22"#O.OOOOO..O#",
23"#O.......OO#",
24"#O..>.....O#",
25"#O....F...O#",
26"#O..OOOOO.O#",
27"#O........O#",
28"#O........O#",
29"#OOOOOOOOOO#",
30"############"],[
31"############",
32"#OOOOOOOOOO#",
33"#O........O#",
34"#O..OO....O#",
35"#O........O#",
36"#OF....>..O#",
37"#O....OO..O#",
38"#O........O#",
39"#O..OO....O#",
40"#O........O#",
41"#OOOOOOOOOO#",
42"############"]]
43const IDOLS=[[
44"############",
45"#..........#",
46"#...ggg....#",
47"#...gIg....#",
48"#...ggg....#",
49"#..........#",
50"#..........#",
51"#..........#",
52"#..........#",
53"#..........#",
54"#....>.....#",
55"############"],[
56"############",
57"#..........#",
58"#....gg....#",
59"#...gIg....#",
60"#....ggg...#",
61"#..........#",
62"#..........#",
63"#..........#",
64"#..........#",
65"#..........#",
66"#....>.....#",
67"############"],[
68"############",
69"#..........#",
70"#...gg.....#",
71"#...gIgg...#",
72"#.....gg...#",
73"#..........#",
74"#..........#",
75"#..........#",
76"#..........#",
77"#..........#",
78"#....>.....#",
79"############"]]
80const IDOLS2=[[
81"############",
82"#..........#",
83"#..ggg.....#",
84"#..gIgg....#",
85"#....gg....#",
86"#..........#",
87"#.....#....#",
88"#..........#",
89"#..........#",
90"#..........#",
91"#...>......#",
92"############"],[
93"############",
94"#..........#",
95"#....ggg...#",
96"#...ggIg...#",
97"#....gg....#",
98"#..........#",
99"#..........#",
100"#....#.....#",
101"#..........#",
102"#..........#",
103"#.....>....#",
104"############"],[
105"############",
106"#..........#",
107"#...gg.....#",
108"#..ggIg....#",
109"#...gg.....#",
110"#..........#",
111"#..........#",
112"#.....#....#",
113"#..........#",
114"#..........#",
115"#....>.....#",
116"############"]]
117const CRACKS=[[
118"############",
119"#..........#",
120"#.####.....#",
121"#....#.....#",
122"#..x.>.....#",
123"#....#.....#",
124"#....#.....#",
125"#..........#",
126"#..........#",
127"#..........#",
128"#..........#",
129"############"],[
130"############",
131"#..........#",
132"#.....####.#",
133"#.....#....#",
134"#.....#....#",
135"#...x.>....#",
136"#.....#....#",
137"#.....####.#",
138"#..........#",
139"#..........#",
140"#..........#",
141"############"],[
142"############",
143"#..........#",
144"#..######..#",
145"#.......#..#",
146"#...x...#..#",
147"#....>..#..#",
148"#.......#..#",
149"#..######..#",
150"#..........#",
151"#..........#",
152"#..........#",
153"############"]]
154const HALLS=[[
155"############",
156"#..........#",
157"#..a.......#",
158"#......b...#",
159"#..........#",
160"#....c.....#",
161"#......d...#",
162"#..........#",
163"#....F.....#",
164"#....>.....#",
165"#..........#",
166"############"],[
167"############",
168"#..........#",
169"#......b...#",
170"#..........#",
171"#..a.......#",
172"#..........#",
173"#....c..d..#",
174"#..........#",
175"#..F.>.....#",
176"#..........#",
177"#..........#",
178"############"],[
179"############",
180"#..........#",
181"#..a....b..#",
182"#..........#",
183"#..........#",
184"#....c..F..#",
185"#..........#",
186"#......d...#",
187"#..........#",
188"#....>.....#",
189"#..........#",
190"############"]]
191const ESCAPES=[[
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"############"]]
228const BEASTS=[[10,8],[9,7]]
229const PITS2=[[
230"############",
231"#OOOOOOOOOO#",
232"#O..FOOO..O#",
233"#O...OO...O#",
234"#O........O#",
235"#O...O>O..O#",
236"#O........O#",
237"#O...OO...O#",
238"#O..OOOO..O#",
239"#O........O#",
240"#OOOOOOOOOO#",
241"############"],[
242"############",
243"#OOOOOOOOOO#",
244"#O......OOO#",
245"#O.OOOOO..O#",
246"#O....O...O#",
247"#O..O>O...O#",
248"#O....F...O#",
249"#O..OOOOO.O#",
250"#O....O...O#",
251"#O........O#",
252"#OOOOOOOOOO#",
253"############"],[
254"############",
255"#OOOOOOOOOO#",
256"#O....O...O#",
257"#O..OOO...O#",
258"#O........O#",
259"#OF..O.>..O#",
260"#O....OOO.O#",
261"#O........O#",
262"#O..OOO...O#",
263"#O....O...O#",
264"#OOOOOOOOOO#",
265"############"],[
266"############",
267"#OOOOOOOOOO#",
268"#O..F.....O#",
269"#O.OOOO...O#",
270"#O.....O..O#",
271"#O..>.OO..O#",
272"#O.....O..O#",
273"#O...OOOO.O#",
274"#O........O#",
275"#O........O#",
276"#OOOOOOOOOO#",
277"############"]]
278const CRACKS2=[[
279"############",
280"#.....x....#",
281"#.#####....#",
282"#....##....#",
283"#..x.>#....#",
284"#....##....#",
285"#....##....#",
286"#.....x....#",
287"#..........#",
288"#..........#",
289"#..........#",
290"############"],[
291"############",
292"#....x.....#",
293"#.....####.#",
294"#....##....#",
295"#....##....#",
296"#...x.>....#",
297"#....##....#",
298"#.....####.#",
299"#....x.....#",
300"#..........#",
301"#..........#",
302"############"],[
303"############",
304"#....x.....#",
305"#..######..#",
306"#.... ..#..#",
307"#...x...#..#",
308"#..x.>..#..#",
309"#.......#..#",
310"#..######..#",
311"#....x.....#",
312"#..........#",
313"#..........#",
314"############"]].map(m=>m.map(r=>r.replace(/ /g,".")))
315const HALLS2=[[
316"############",
317"#..........#",
318"#..a...b...#",
319"#..........#",
320"#....F.....#",
321"#....c.....#",
322"#......d...#",
323"#..........#",
324"#....>.....#",
325"#..........#",
326"#..........#",
327"############"],[
328"############",
329"#..........#",
330"#....b.....#",
331"#..a.......#",
332"#....F.....#",
333"#....c..d..#",
334"#..........#",
335"#....>.....#",
336"#..........#",
337"#..........#",
338"#..........#",
339"############"],[
340"############",
341"#..........#",
342"#..a....b..#",
343"#....F.....#",
344"#..........#",
345"#....c.....#",
346"#......d...#",
347"#..........#",
348"#....>.....#",
349"#..........#",
350"#..........#",
351"############"]]
352const ESCAPES2=[[
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"############"]]
389const BEASTS2=[[9,8],[9,7],[10,7]]
390const PITS3=[[
391"############",
392"#OOOOOOOOOO#",
393"#O..SFOO..O#",
394"#O...OO...O#",
395"#O........O#",
396"#O...O>O..O#",
397"#O........O#",
398"#O...OO...O#",
399"#O..OOOO..O#",
400"#O........O#",
401"#OOOOOOOOOO#",
402"############"],[
403"############",
404"#OOOOOOOOOO#",
405"#O.....SOOO#",
406"#O.OOOOO..O#",
407"#O....O...O#",
408"#O..O>O...O#",
409"#O....F...O#",
410"#O..OOOOO.O#",
411"#O....O...O#",
412"#O........O#",
413"#OOOOOOOOOO#",
414"############"],[
415"############",
416"#OOOOOOOOOO#",
417"#O....O...O#",
418"#O..OOO...O#",
419"#O....S...O#",
420"#OF..O.>..O#",
421"#O....OOO.O#",
422"#O........O#",
423"#O..OOO...O#",
424"#O....O...O#",
425"#OOOOOOOOOO#",
426"############"],[
427"############",
428"#OOOOOOOOOO#",
429"#O..S.O...O#",
430"#O..OOO...O#",
431"#O........O#",
432"#O...O.>..O#",
433"#O....OOO.O#",
434"#O....F...O#",
435"#O..OOO...O#",
436"#O....O...O#",
437"#OOOOOOOOOO#",
438"############"]]
439const IDOLS3=[[
440"############",
441"#..........#",
442"#..ggg.....#",
443"#..gISg....#",
444"#....gg....#",
445"#..........#",
446"#.....#....#",
447"#..........#",
448"#..........#",
449"#..........#",
450"#...>......#",
451"############"],[
452"############",
453"#..........#",
454"#....ggg...#",
455"#...gSIg...#",
456"#....gg....#",
457"#..........#",
458"#..........#",
459"#....#.....#",
460"#..........#",
461"#..........#",
462"#.....>....#",
463"############"],[
464"############",
465"#..........#",
466"#...gg.....#",
467"#..gSIg....#",
468"#...gg.....#",
469"#..........#",
470"#..........#",
471"#.....#....#",
472"#..........#",
473"#..........#",
474"#....>.....#",
475"############"]]
476const CRACKS3=[[
477"############",
478"#....Sx....#",
479"#.#####....#",
480"#....##....#",
481"#..x.>#....#",
482"#....##....#",
483"#....##....#",
484"#.....x....#",
485"#..........#",
486"#..........#",
487"#..........#",
488"############"],[
489"############",
490"#....x.....#",
491"#...S.####.#",
492"#....##....#",
493"#....##....#",
494"#...x.>....#",
495"#....##....#",
496"#.....####.#",
497"#....x.....#",
498"#..........#",
499"#..........#",
500"############"],[
501"############",
502"#....x.....#",
503"#..######..#",
504"#....S..#..#",
505"#...x...#..#",
506"#..x.>..#..#",
507"#.......#..#",
508"#..######..#",
509"#....x.....#",
510"#..........#",
511"#..........#",
512"############"]]
513const HALLS3=[[
514"############",
515"#..........#",
516"#..a..S.b..#",
517"#..........#",
518"#....F.....#",
519"#....c.....#",
520"#......d...#",
521"#..........#",
522"#....>.....#",
523"#..........#",
524"#..........#",
525"############"],[
526"############",
527"#..........#",
528"#....b..S..#",
529"#..a.......#",
530"#....F.....#",
531"#....c..d..#",
532"#..........#",
533"#....>.....#",
534"#..........#",
535"#..........#",
536"#..........#",
537"############"],[
538"############",
539"#..........#",
540"#..a....b..#",
541"#....F..S..#",
542"#..........#",
543"#....c.....#",
544"#......d...#",
545"#..........#",
546"#....>.....#",
547"#..........#",
548"#..........#",
549"############"],[
550"############",
551"#..........#",
552"#..a..S.b..#",
553"#....F.....#",
554"#..........#",
555"#....c.....#",
556"#......d...#",
557"#..........#",
558"#....>..F..#",
559"#..........#",
560"#..........#",
561"############"]]
562const ESCAPES3=[[
563"############",
564"#....##....#",
565"#..........#",
566"#....S.....#",
567"#...>......#",
568"#..........#",
569"#..##......#",
570"#..........#",
571"#......##..#",
572"#..........#",
573"#..........#",
574"############"],[
575"############",
576"#..........#",
577"#...##.....#",
578"#....S.....#",
579"#....>.....#",
580"#..........#",
581"#.....##...#",
582"#..........#",
583"#..##......#",
584"#..........#",
585"#..........#",
586"############"],[
587"############",
588"#..........#",
589"#..##......#",
590"#....S.....#",
591"#..>....##.#",
592"#..........#",
593"#......##..#",
594"#..........#",
595"#..........#",
596"#..........#",
597"#..........#",
598"############"],[
599"############",
600"#....##....#",
601"#..........#",
602"#..S.......#",
603"#....>.....#",
604"#..........#",
605"#..##..S...#",
606"#..........#",
607"#......##..#",
608"#..........#",
609"#..........#",
610"############"]]
611const BEASTS3=[[9,8],[9,7],[9,6],[10,7]]
612const ESCAPES4=[[
613"############",
614"#....##....#",
615"#....S.....#",
616"#..........#",
617"#...>......#",
618"#..........#",
619"#..##......#",
620"#......S...#",
621"#......##..#",
622"#..........#",
623"#..........#",
624"############"],[
625"############",
626"#..........#",
627"#...##.....#",
628"#....S.....#",
629"#..>.......#",
630"#..........#",
631"#.....##...#",
632"#.....S....#",
633"#..........#",
634"#..........#",
635"#..........#",
636"############"],[
637"############",
638"#..........#",
639"#..##......#",
640"#....S.....#",
641"#..>....##.#",
642"#..........#",
643"#......##..#",
644"#.....S....#",
645"#..........#",
646"#..........#",
647"#..........#",
648"############"],[
649"############",
650"#....##....#",
651"#..........#",
652"#..S.......#",
653"#....>.....#",
654"#..........#",
655"#..##..S...#",
656"#..........#",
657"#......##..#",
658"#..........#",
659"#..........#",
660"############"]]
661const BEASTS4=[[9,8],[10,8],[9,7],[10,7]]
662const PITS4=[[
663"############",
664"#OOOOOOOOOO#",
665"#O..SFOO..O#",
666"#O.O.OO...O#",
667"#O........O#",
668"#O..OO>O..O#",
669"#O........O#",
670"#O...OO...O#",
671"#O..OOOO..O#",
672"#O........O#",
673"#OOOOOOOOOO#",
674"############"],[
675"############",
676"#OOOOOOOOOO#",
677"#O....SO..O#",
678"#O.OOOOO..O#",
679"#O....O...O#",
680"#O..O>O...O#",
681"#O....F...O#",
682"#O..OOOOO.O#",
683"#O....O...O#",
684"#O........O#",
685"#OOOOOOOOOO#",
686"############"],[
687"############",
688"#OOOOOOOOOO#",
689"#O....O...O#",
690"#O..OOO...O#",
691"#O....S...O#",
692"#OF..O.>..O#",
693"#O....OOO.O#",
694"#O........O#",
695"#O..OOO...O#",
696"#O....O...O#",
697"#OOOOOOOOOO#",
698"############"],[
699"############",
700"#OOOOOOOOOO#",
701"#O..F.O...O#",
702"#O..OOO...O#",
703"#O....S...O#",
704"#O...O.>..O#",
705"#O....OOO.O#",
706"#O........O#",
707"#O..OOO...O#",
708"#O....O...O#",
709"#OOOOOOOOOO#",
710"############"]]
711const IDOLS4=[[
712"############",
713"#..........#",
714"#..ggg.....#",
715"#..gISg....#",
716"#...Sgg....#",
717"#..........#",
718"#.....#....#",
719"#..........#",
720"#..........#",
721"#..........#",
722"#...>......#",
723"############"],[
724"############",
725"#..........#",
726"#....ggg...#",
727"#...gSIg...#",
728"#....gg....#",
729"#....S.....#",
730"#..........#",
731"#....#.....#",
732"#..........#",
733"#..........#",
734"#.....>....#",
735"############"],[
736"############",
737"#..........#",
738"#...gg.....#",
739"#..gSIg....#",
740"#...gg..S..#",
741"#..........#",
742"#..........#",
743"#.....#....#",
744"#..........#",
745"#..........#",
746"#....>.....#",
747"############"]]
748const CRACKS4=[[
749"############",
750"#....Sx....#",
751"#.#####....#",
752"#....##....#",
753"#..x.>#....#",
754"#....##....#",
755"#....##....#",
756"#..x.......#",
757"#..........#",
758"#..........#",
759"#..........#",
760"############"],[
761"############",
762"#....x.....#",
763"#...S.####.#",
764"#....##....#",
765"#....##....#",
766"#..xx.>....#",
767"#....##....#",
768"#.....####.#",
769"#....x.....#",
770"#..........#",
771"#..........#",
772"############"],[
773"############",
774"#....x.....#",
775"#..######..#",
776"#....S..#..#",
777"#...x...#..#",
778"#..x.>..#..#",
779"#...x...#..#",
780"#..######..#",
781"#....x.....#",
782"#..........#",
783"#..........#",
784"############"],[
785"############",
786"#..S.x.....#",
787"#..######..#",
788"#.......#..#",
789"#..xx...#..#",
790"#....>..#..#",
791"#.......#..#",
792"#..######..#",
793"#....x.....#",
794"#..........#",
795"#..........#",
796"############"]]
797const HALLS4=[[
798"############",
799"#..........#",
800"#..a..S.b..#",
801"#..........#",
802"#....F.....#",
803"#....c.....#",
804"#......d...#",
805"#.....S....#",
806"#....>.....#",
807"#..........#",
808"#..........#",
809"############"],[
810"############",
811"#..........#",
812"#....b..S..#",
813"#..a.......#",
814"#....F.....#",
815"#....c..d..#",
816"#.....S....#",
817"#....>.....#",
818"#..........#",
819"#..........#",
820"#..........#",
821"############"],[
822"############",
823"#..........#",
824"#..a....b..#",
825"#....F..S..#",
826"#..........#",
827"#....c.....#",
828"#......d...#",
829"#.....S....#",
830"#....>.....#",
831"#..........#",
832"#..........#",
833"############"],[
834"############",
835"#..........#",
836"#..a..S.b..#",
837"#....F.....#",
838"#..........#",
839"#....c.....#",
840"#...S..d...#",
841"#..........#",
842"#....>..F..#",
843"#..........#",
844"#..........#",
845"############"]]
846const ESCAPES5=[[
847"############",
848"#....##....#",
849"#....S.....#",
850"#..........#",
851"#...>......#",
852"#..........#",
853"#..##......#",
854"#..........#",
855"#......##..#",
856"#....S.....#",
857"#..........#",
858"############"],[
859"############",
860"#..........#",
861"#...##.....#",
862"#....S.....#",
863"#..>.......#",
864"#..........#",
865"#.....##...#",
866"#....S.....#",
867"#..........#",
868"#..........#",
869"#..........#",
870"############"],[
871"############",
872"#..........#",
873"#..##......#",
874"#....S.....#",
875"#..>....##.#",
876"#..........#",
877"#......##..#",
878"#....S.....#",
879"#..........#",
880"#..........#",
881"#..........#",
882"############"],[
883"############",
884"#....##....#",
885"#..........#",
886"#..S.......#",
887"#....>.....#",
888"#..........#",
889"#..##..S...#",
890"#..........#",
891"#......##..#",
892"#....S.....#",
893"#..........#",
894"############"]]
895const BEASTS5=[[10,8],[10,7],[9,7],[9,6]]
896const ESCAPES6=[[
897"############",
898"#....##....#",
899"#....S.....#",
900"#..........#",
901"#...>......#",
902"#....S.....#",
903"#..##......#",
904"#......S...#",
905"#......##..#",
906"#..........#",
907"#..........#",
908"############"],[
909"############",
910"#..........#",
911"#...##.....#",
912"#....S.....#",
913"#..>.......#",
914"#.....S....#",
915"#.....##...#",
916"#.....S....#",
917"#..........#",
918"#..........#",
919"#..........#",
920"############"],[
921"############",
922"#..........#",
923"#..##......#",
924"#....S.....#",
925"#..>....##.#",
926"#.....S....#",
927"#......##..#",
928"#.....S....#",
929"#..........#",
930"#..........#",
931"#..........#",
932"############"],[
933"############",
934"#....##....#",
935"#....S.....#",
936"#..S.......#",
937"#....>.....#",
938"#..........#",
939"#..##..S...#",
940"#..........#",
941"#......##..#",
942"#..........#",
943"#..........#",
944"############"]]
945const BEASTS6=[[10,8],[10,7],[9,7],[10,6]]
946const LEVEL_ORDER=[1,2,3,4,5,6]
947const LEVEL_ARCS={1:"foundation",2:"pressure",3:"ward",4:"hunt",5:"hollow",6:"finale",7:"afterglow"}
948const MODES={
949 standard:{label:"standard",desc:"full campaign"},
950 iron:{label:"iron",desc:"early beast"},
951 greed:{label:"greed",desc:"hot relics, costly gold"},
952 dark:{label:"dark",desc:"weak flame, fast dread"},
953 daily:{label:"daily",desc:"fixed daily seed"},
954 heavy:{label:"heavy torch",desc:"starts lit, more fuel"},
955 swift:{label:"swift",desc:"pits never catch you"},
956 warded:{label:"warded",desc:"starts warded, wards hold longer"}
957}
958const MODE_IDS=Object.keys(MODES)
959const MODS={
960 thin:{label:"thin flame",desc:"torches burn faster"},
961 wake:{label:"wakeful beast",desc:"the beast starts hotter"},
962 rich:{label:"rich relics",desc:"more relics surface"},
963 gloom:{label:"heavy dread",desc:"darkness grips sooner"},
964 grace:{label:"strong shrines",desc:"wards hold longer"}
965}
966const MOD_IDS=Object.keys(MODS)
967const FLOOR_TEXT={
968 1:["A first descent through stale rites.","The old ruin still pretends to be fair."],
969 2:["Pressure arrives before understanding catches up.","Every branch feels closer to pursuit."],
970 3:["Wards answer, but never for free.","Blessing and burden share the same stone."],
971 4:["The hunt floor wants you heard.","Noise becomes destiny down here."],
972 5:["The hollow floor strips runs to their bones.","Only the sharpest choices still matter."],
973 6:["The last gate weighs what you became.","Every surviving system cashes in here."],
974 7:["Dawn should have ended this. The ruin has other plans.","Nothing left down here owes you mercy."]
975}
976const FLOOR_THEMES={
977 1:{name:"foundation",tag:"stale rite",accent:"verdigris",fog:"soft lantern fog"},
978 2:{name:"pressure",tag:"ember choke",accent:"rust flare",fog:"hot dust"},
979 3:{name:"ward",tag:"cold liturgy",accent:"aqua ward",fog:"glass mist"},
980 4:{name:"hunt",tag:"rose alarm",accent:"pink hunt",fog:"thin red haze"},
981 5:{name:"hollow",tag:"ash hush",accent:"bone gray",fog:"dry blackout"},
982 6:{name:"finale",tag:"crown weather",accent:"gold ruin",fog:"judging dark"},
983 7:{name:"afterglow",tag:"borrowed dawn",accent:"ash gold",fog:"embered haze"}
984}
985const ENEMY_TEXT={
986 beast:["A one-eyed pursuer that reads space better than fear.","It wants a lane, not a duel."],
987 shade:["A cold pass-through that feeds dread and tells on you.","It does not block the path; it poisons it."],
988 watcher:["A fixed witness that turns position into pursuit.","Quiet fails where it stands."],
989 leech:["A grave thing that eats ward and heat.","It survives by thinning what protects you."],
990 effigy:["A second relic, and a third. Only one is true.","The others were never meant to be found."]
991}
992const SHRINE_TEXT=[
993 "The shrine lends shape to your nerve.",
994 "A ward forms, thin and listening.",
995 "The shrine answers, and the answer is costly."
996]
997const BRAZIER_TEXT=[
998 "The brazier spits up old warmth.",
999 "A hungry flame leans toward your torch.",
1000 "The coals remember better fires than yours."
1001]
1002const CURSE_TEXT=[
1003 "The stone bites back.",
1004 "A mean seam in the floor wakes underfoot.",
1005 "The room answers touch with dread."
1006]
1007const SFX_MAP={
1008 hit:{freq:180,dur:.08,type:"square"},
1009 beast:{freq:90,dur:.12,type:"sawtooth"},
1010 win:{freq:660,dur:.4,type:"sine"},
1011 relic:{freq:520,dur:.15,type:"triangle"},
1012 ward:{freq:340,dur:.2,type:"sine"},
1013 shift:{freq:220,dur:.1,type:"sine"},
1014 dead:{freq:70,dur:.5,type:"sawtooth"}
1015}
1016const FINALE_NAMES=["Ash Gate","Gold Gate","Hush Gate","Ward Gate"]
1017const INTRO_TEXT=["Move with arrows or WASD.","Carry flame. Bow for gold.","Wards hold. Cracks strip. The beast reads haste."].join("\n")
1018const HINT_TEXT={
1019 torch:"Take the torch early. Darkness taxes every step without it.",
1020 ward:"Shrines raise ward. Ward softens dark pressure, then burns down.",
1021 crack:"Cracks are fast but costly. Mask or ward makes the crossing safer.",
1022 beast:"Fire slows pursuit. A dropped torch can buy one clean lane."
1023}
1024const RELICS={
1025 ember:{name:"ember heart",label:"ember",up:"slower torch drain",down:"beast ignores dropped flame"},
1026 tooth:{name:"gold tooth",label:"tooth",up:"gold slips the crack",down:"beast enrages sooner"},
1027 hush:{name:"hush bell",label:"hush",up:"dread stays low",down:"fire never slows beast"},
1028 eye:{name:"idol eye",label:"eye",up:"idol bows easier, shrines strengthen",down:"crown scorches torch"},
1029 brand:{name:"night brand",label:"brand",up:"shades break on touch",down:"cursed ground bites harder"},
1030 wick:{name:"long wick",label:"wick",up:"sight and dropped flame last longer",down:"carried torch burns hotter"},
1031 chain:{name:"ward chain",label:"chain",up:"wards and shrines linger",down:"gold no longer softens torch loss"},
1032 veil:{name:"veil thread",label:"veil",up:"mask protects torch through the crack",down:"shrines grant less"},
1033 greave:{name:"pit greaves",label:"greave",up:"pit footing stays sure",down:"crack crossings cost extra"},
1034 lure:{name:"lure spool",label:"lure",up:"dropped flame always pulls the beast",down:"carried fire never slows it"},
1035 salt:{name:"salt sigil",label:"salt",up:"watchers and curses bite softer",down:"shades drain more torch"},
1036 ash:{name:"ash lung",label:"ash",up:"braziers flare stronger",down:"shrines sip your torch"}
1037}
1038const RELIC_IDS=Object.keys(RELICS)
1039const RELIC_SPOTS={
10401:[["b",7,2],["v",8,8],["h",6,6]],
10412:[["h",6,6],["v",5,8],["p",8,8]],
10423:[["i",2,8],["c",2,1],["v",8,8],["h",6,6]],
10434:[["h",5,6],["i",5,8],["p",8,8],["v",8,8]],
10445:[["h",5,6],["i",5,8],["c",2,1],["v",5,8],["p",8,8]],
10456:[["e",7,3],["e",9,5],["e",5,8]],
10467:[["h",5,6],["i",5,8],["c",2,1],["v",5,8],["p",8,8]]
1047}
1048const EFFIGY_SPOTS={
10491:[],
10502:[["v",9,8]],
10513:[["v",9,8]],
10524:[["v",9,8]],
10535:[["v",9,8]],
10546:[],
10557:[["v",9,8]]
1056}
1057const BRAZIER_SPOTS={
10581:[["h",8,8],["p",2,8]],
10592:[["h",8,8],["v",2,8],["c",8,8]],
10603:[["h",8,8],["i",8,8],["p",2,8],["v",2,8]],
10614:[["h",8,8],["i",8,8],["p",2,8],["v",2,8]],
10625:[["h",8,8],["i",8,8],["p",2,8],["v",2,8],["c",8,8]],
10636:[["e",8,2],["e",2,8],["e",8,8]],
10647:[["h",8,8],["i",8,8],["p",2,8],["v",2,8],["c",8,8]]
1065}
1066const SHRINE_SPOTS={
10671:[["i",2,8]],
10682:[["h",8,4],["v",8,8]],
10693:[["h",8,4],["i",8,8],["c",8,8]],
10704:[["h",8,4],["i",8,8],["c",8,8],["p",8,4]],
10715:[["h",8,4],["i",8,8],["c",8,8],["v",8,8],["p",8,4]],
10726:[["e",8,4],["e",3,3],["e",8,8]],
10737:[["h",8,4],["i",8,8],["c",8,8],["v",8,8],["p",8,4]]
1074}
1075const CURSE_SPOTS={
10761:[["h",8,8],["p",8,2],["c",8,8]],
10772:[["h",7,4],["c",2,1],["p",8,8],["v",8,4]],
10783:[["h",7,4],["i",8,8],["c",7,1],["p",8,8],["v",8,4]],
10794:[["h",7,4],["i",8,8],["c",7,1],["p",8,8],["v",8,4]],
10805:[["h",7,4],["i",8,8],["c",7,1],["p",8,8],["v",8,4],["b",8,8]],
10816:[["e",8,8],["e",2,2],["e",8,2],["e",3,8]],
10827:[["h",7,4],["i",8,8],["c",7,1],["p",8,8],["v",8,4],["b",8,8]]
1083}
1084const SHADE_SPOTS={
10851:[["h",9,4],["p",8,8]],
10862:[["h",9,4],["c",8,8],["v",8,8]],
10873:[["h",9,4],["i",8,8],["c",8,8],["p",8,4]],
10884:[["h",9,4],["i",8,8],["c",8,8],["p",8,4],["v",8,8]],
10895:[["h",9,4],["i",8,8],["c",8,8],["p",8,4],["v",8,8],["b",8,8]],
10906:[["e",8,8],["e",2,8],["e",8,2],["e",3,8]],
10917:[["h",9,4],["i",8,8],["c",8,8],["p",8,4],["v",8,8],["b",8,8]]
1092}
1093const BLOCK_SPOTS={
10941:[],
10952:[["h",7,8],["p",2,8],["v",2,4]],
10963:[["h",7,8],["p",2,8],["c",8,2],["v",2,4]],
10974:[["h",7,8],["p",2,8],["c",8,2],["v",2,4],["b",8,8]],
10985:[["h",7,8],["p",2,8],["c",8,2],["v",2,4],["b",8,8],["i",8,4]],
10996:[["e",2,4],["e",7,7],["e",2,7]],
11007:[["h",7,8],["p",2,8],["c",8,2],["v",2,4],["b",8,8],["i",8,4]]
1101}
1102const WATCHER_SPOTS={
11031:[],
11042:[["h",8,8],["v",8,8]],
11053:[["h",8,8],["c",2,8],["v",8,8]],
11064:[["h",8,8],["c",2,8],["p",8,8],["v",8,8]],
11075:[["h",8,8],["c",2,8],["p",8,8],["v",8,8],["i",8,8]],
11086:[["e",8,2],["e",2,8]],
11097:[["h",8,8],["c",2,8],["p",8,8],["v",8,8],["i",8,8]]
1110}
1111const LEECH_SPOTS={
11121:[],
11132:[],
11143:[["p",8,8],["c",8,8]],
11154:[["p",8,8],["c",8,8],["v",8,8]],
11165:[["p",8,8],["c",8,8],["v",8,8],["h",8,4]],
11176:[["e",8,8],["e",3,3]],
11187:[["p",8,8],["c",8,8],["v",8,8],["h",8,4]]
1119}
1120const LEVELS={
11211:{pits:PITS,idols:IDOLS,cracks:CRACKS,halls:HALLS,escapes:ESCAPES,beasts:BEASTS,dark:"pecv",tone:"The air is stale with old rites."},
11222:{pits:PITS2,idols:IDOLS2,cracks:CRACKS2,halls:HALLS2,escapes:ESCAPES2,beasts:BEASTS2,dark:"behipcv",tone:"The ruin tightens. It knows your name now."},
11233:{pits:PITS3,idols:IDOLS3,cracks:CRACKS3,halls:HALLS3,escapes:ESCAPES3,beasts:BEASTS3,dark:"behipcv",tone:"The shrines still answer, but every blessing exacts a price."},
11244:{pits:PITS4,idols:IDOLS4,cracks:CRACKS4,halls:HALLS4,escapes:ESCAPES4,beasts:BEASTS4,dark:"behipcv",tone:"The hunt floor listens for every sound you make."},
11255:{pits:PITS4,idols:IDOLS4,cracks:CRACKS4,halls:HALLS4,escapes:ESCAPES5,beasts:BEASTS5,dark:"behipcv",tone:"The hollow floor strips away whatever was merely enough."},
11266:{pits:PITS4,idols:IDOLS4,cracks:CRACKS4,halls:HALLS4,escapes:ESCAPES6,beasts:BEASTS6,dark:"behipcv",tone:"The final gate wakes. Everything left in you will be tested."},
11277:{pits:PITS4,idols:IDOLS4,cracks:CRACKS4,halls:HALLS4,escapes:ESCAPES6,beasts:BEASTS6,dark:"behipcv",tone:"You already won. The ruin didn't agree to let you leave."}
1128}
1129const BASE={
1130g:{m:[
1131"############",
1132"#..........#",
1133"#..L.....R.#",
1134"#..........#",
1135"#..........#",
1136"#....>.....#",
1137"#..........#",
1138"#..........#",
1139"#..........#",
1140"#..........#",
1141"#..........#",
1142"############"],ex:{">":"h",L:"b",R:"e"}},
1143b:{m:[
1144"############",
1145"#..........#",
1146"#...MT.....#",
1147"#..........#",
1148"#..........#",
1149"#.....>....#",
1150"#..........#",
1151"#..........#",
1152"#..........#",
1153"#..........#",
1154"#..........#",
1155"############"],ex:{">":"h"}},
1156v:{m:[
1157"############",
1158"#..........#",
1159"#....C.K...#",
1160"#..........#",
1161"#..........#",
1162"#..........#",
1163"#..........#",
1164"#..........#",
1165"#..........#",
1166"#....>.....#",
1167"#..........#",
1168"############"],ex:{">":"e"}},
1169d:{m:["############","#..........#","#..........#","#..........#","#....X.....#","#..........#","#..........#","#..........#","#..........#","#..........#","#..........#","############"],ex:{}}
1170}
1171const BASE2={
1172g:{m:[
1173"############",
1174"#..........#",
1175"#..L..#..R.#",
1176"#..........#",
1177"#....#.....#",
1178"#....>.....#",
1179"#..........#",
1180"#.....#....#",
1181"#..........#",
1182"#..........#",
1183"#..........#",
1184"############"],ex:{">":"h",L:"b",R:"e"}},
1185b:{m:[
1186"############",
1187"#..........#",
1188"#...MT.....#",
1189"#....#.....#",
1190"#..........#",
1191"#..#..>....#",
1192"#..........#",
1193"#.....#....#",
1194"#..........#",
1195"#..........#",
1196"#..........#",
1197"############"],ex:{">":"h"}},
1198v:{m:[
1199"############",
1200"#..........#",
1201"#...#C.K...#",
1202"#..........#",
1203"#....#.....#",
1204"#..........#",
1205"#.....#....#",
1206"#..........#",
1207"#..........#",
1208"#...>......#",
1209"#..........#",
1210"############"],ex:{">":"e"}},
1211d:BASE.d
1212}
1213const BASE3={
1214g:{m:[
1215"############",
1216"#..........#",
1217"#..L..S..R.#",
1218"#..........#",
1219"#....#.....#",
1220"#....>.....#",
1221"#..........#",
1222"#.....#....#",
1223"#..........#",
1224"#..........#",
1225"#..........#",
1226"############"],ex:{">":"h",L:"b",R:"e"}},
1227b:{m:[
1228"############",
1229"#..........#",
1230"#..SMT.....#",
1231"#....#.....#",
1232"#..........#",
1233"#..#..>....#",
1234"#..........#",
1235"#.....#....#",
1236"#..........#",
1237"#..........#",
1238"#..........#",
1239"############"],ex:{">":"h"}},
1240v:{m:[
1241"############",
1242"#..........#",
1243"#...#C.K.S.#",
1244"#..........#",
1245"#....#.....#",
1246"#..........#",
1247"#.....#....#",
1248"#..........#",
1249"#..........#",
1250"#...>......#",
1251"#..........#",
1252"############"],ex:{">":"e"}},
1253d:BASE.d
1254}
1255const BASES={1:BASE,2:BASE2,3:BASE3,4:BASE3,5:BASE3,6:BASE3,7:BASE3}
1mb/index.html added +957
@@ -0,0 +1,957 @@
1<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>1MB Roguelike v1</title>
2<style>
3:root{color-scheme:dark}
4body{margin:0;min-width:100vw;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#020403;color:#dbe8d7;font:14px/1.3 monospace}
5#s{--bg0:#07100b;--bg1:#010201;--panel:#07110cdd;--panelEdge:#29432d;--gridEdge:#213d28;--accent:#99d9a4;--accent2:#f0c86a;--fog:#d8f1db;--mist:#7fb48622;--glow:#9be8a055;--gridGlow:#9be8a022;--text:#deeddc;--muted:#8aa38f;--floor:#112015;--memory:#5f7b66;--hidden:#081009;--wall:#647569;--pit:#a4473e;--exit:#f7d37f;--torch:#ffbd63;--shrine:#9df3e8;--relic:#ffe27b;--hazard:#ff8777;--shade:#9bc1ff;--watcher:#ffd294;--leech:#8ef3da;--beast:#ff77d2;--player:#f8fff6;--shadow:rgba(0,0,0,.8);--wardGlow:#88fff330;width:100%;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:18px 14px 24px;overflow-x:hidden;box-sizing:border-box;background:
6radial-gradient(circle at 50% 16%,var(--mist),transparent 30%),
7radial-gradient(circle at 50% 45%,var(--gridGlow),transparent 46%),
8linear-gradient(180deg,var(--bg0),var(--bg1) 72%);color:var(--text);transition:background .2s,padding .15s,color .15s}
9#u,#i,#l,#g,#r{box-sizing:border-box}
10#u{min-height:42px;width:fit-content;max-width:min(92vw,72ch);padding:9px 14px;border:1px solid color-mix(in srgb,var(--panelEdge) 72%,#5b6a60);background:linear-gradient(180deg,#0b1510d8,#050805f0);box-shadow:0 0 0 1px #000 inset,0 0 18px #0006;text-align:center;white-space:pre-line;color:var(--text);letter-spacing:.04em}
11#u small{display:block;color:var(--muted);margin-top:3px;font-size:12px;letter-spacing:.14em;text-transform:uppercase}
12#p{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:center;gap:16px;width:min(100%,860px);min-height:214px;max-width:100%;overflow:visible}
13#g{position:relative;display:grid;grid-template-columns:repeat(12,16px);gap:1px;padding:12px;border:1px solid var(--gridEdge);background:
14radial-gradient(circle at 50% 50%,color-mix(in srgb,var(--bg0) 72%,#0d1510) 0,var(--shadow) 58%,#000 88%),
15linear-gradient(180deg,#060806,#010101);box-shadow:inset 0 0 0 1px #000,0 0 24px #0008,0 0 10px var(--gridGlow);transition:transform .12s,box-shadow .12s,opacity .12s,filter .15s;flex:0 0 auto;max-width:100%}
16#g:before{content:"";position:absolute;inset:0;pointer-events:none;background:
17radial-gradient(circle at 50% 50%,transparent 38%,var(--shadow) 76%),
18linear-gradient(180deg,transparent,rgba(0,0,0,.12));mix-blend-mode:screen;opacity:.72}
19#g[data-intro]:after{content:attr(data-intro);position:absolute;left:10px;right:10px;top:10px;padding:10px 12px;border:1px solid color-mix(in srgb,var(--accent) 58%,#5b6a60);background:linear-gradient(180deg,#0b130edc,#050805f3);box-shadow:inset 0 0 0 1px #000,0 0 18px #0007;color:var(--fog);white-space:pre-line;line-height:1.35;letter-spacing:.04em;text-align:left;pointer-events:none;z-index:3}
20#g.splash{display:block;flex:0 1 auto;width:min(calc(35ch + 36px),100%);min-width:calc(35ch + 36px);max-width:100%;padding:16px 18px;white-space:pre;line-height:1.28;text-align:left;color:var(--text);overflow:hidden}
21#g.splash:before{opacity:.45}
22#i{flex:0 1 auto;width:min(calc(35ch + 26px),100%);min-width:calc(35ch + 26px);max-width:100%;min-height:230px;padding:11px 13px;border:1px solid color-mix(in srgb,var(--panelEdge) 74%,#5b6a60);background:linear-gradient(180deg,var(--panel),#050805f2);color:var(--text);line-height:1.42;white-space:pre-line;text-align:left;box-shadow:inset 0 0 0 1px #000,0 0 18px #0006;overflow-wrap:break-word}
23#l{min-height:38px;width:min(100%,860px);padding:9px 16px;border:1px solid color-mix(in srgb,var(--panelEdge) 72%,#5b6a60);background:linear-gradient(180deg,#0a110dcd,#050805ec);color:var(--muted);white-space:pre-line;text-align:center;box-shadow:inset 0 0 0 1px #000}
24#r{margin-top:8px;padding:8px 14px;border:1px solid color-mix(in srgb,var(--accent) 70%,#6e7c73);background:linear-gradient(180deg,#09110b,#030503);color:var(--accent);font:inherit;letter-spacing:.08em;text-transform:uppercase;cursor:pointer;box-shadow:0 0 14px #0007}
25#d{display:none;margin-top:8px;grid-template-columns:repeat(3,56px);grid-template-rows:repeat(3,56px);gap:6px}
26#d button{font:16px/1 inherit;border:1px solid color-mix(in srgb,var(--accent) 70%,#6e7c73);background:linear-gradient(180deg,#09110b,#030503);color:var(--accent);cursor:pointer;box-shadow:0 0 14px #0007;display:flex;align-items:center;justify-content:center;touch-action:manipulation}
27#d .pu{grid-column:2;grid-row:1}#d .pl{grid-column:1;grid-row:2}#d .pw{grid-column:2;grid-row:2}#d .pr{grid-column:3;grid-row:2}#d .pd{grid-column:2;grid-row:3}
28.t{position:relative;width:16px;height:16px;display:flex;align-items:center;justify-content:center;transition:transform .08s,color .12s,opacity .12s,filter .12s,text-shadow .12s,background .12s}
29.v{opacity:1;filter:saturate(1)}.m{opacity:.44;filter:saturate(.42) brightness(.78)}.u{opacity:0}
30.far{opacity:.58}.mid{opacity:.8}
31.p{color:var(--player);text-shadow:0 0 9px color-mix(in srgb,var(--player) 30%,var(--glow));font-weight:bold;filter:drop-shadow(0 0 4px #fff3)}.pm{animation:step .12s}.bm{animation:step .12s reverse}
32.w{color:var(--wall)}.o{color:var(--pit);text-shadow:0 0 6px #4d100c}.f{color:var(--floor)}
33.i,.C,.K{color:var(--exit);text-shadow:0 0 6px color-mix(in srgb,var(--exit) 50%,#000)}.g{color:var(--accent2);text-shadow:0 0 6px color-mix(in srgb,var(--accent2) 45%,#000)}.I{color:#ffe8a7;text-shadow:0 0 6px #8f7440}.M{color:#d6ddd6}.T,.F{color:var(--torch);text-shadow:0 0 8px color-mix(in srgb,var(--torch) 60%,#000);animation:flicker .9s infinite}.drop{color:#ffd79d}
34.B{color:var(--beast);text-shadow:0 0 8px color-mix(in srgb,var(--beast) 70%,#000);font-weight:bold}.B.alert{color:#ffa8e4}.B.enraged{color:#ff6767;text-shadow:0 0 8px #ff4d4d}
35.S{color:var(--shrine);text-shadow:0 0 8px color-mix(in srgb,var(--shrine) 65%,#000);animation:pulse 1.5s infinite;font-weight:bold}.S.stress{color:#c5fbff;animation:pulse .6s infinite}
36.r{color:var(--relic);text-shadow:0 0 7px color-mix(in srgb,var(--relic) 55%,#000);font-weight:bold}
37.x,.z{color:var(--hazard);text-shadow:0 0 7px color-mix(in srgb,var(--hazard) 40%,#000);font-weight:bold}.n{color:var(--shade);text-shadow:0 0 6px #22395f}.q{color:var(--watcher);text-shadow:0 0 6px #5a3d12;font-weight:bold}.l{color:var(--leech);text-shadow:0 0 6px #10453b}
38.n.alert,.q.alert,.l.alert{animation:pulse .65s infinite}
39.warded{box-shadow:inset 0 0 0 1px var(--wardGlow);background:radial-gradient(circle at 50% 50%,#ffffff08,transparent 72%)}
40.haz{filter:brightness(1.14)}
41.fog1{opacity:.9}.fog2{opacity:.72}.fog3{opacity:.55}
42#g.hit{transform:translateX(2px);box-shadow:inset 0 0 0 1px #000,0 0 24px #0008,0 0 14px color-mix(in srgb,var(--hazard) 70%,#000)}#g.win{box-shadow:inset 0 0 0 1px #000,0 0 24px #0008,0 0 14px var(--glow)}#g.shift{transform:scale(1.02)}#g.ward{animation:wardbreak .2s ease-out}#g.relic{animation:relicpulse .24s ease-out}
43#u.dead,#u.win,#u.camp{letter-spacing:.12em;text-transform:uppercase}
44#s[data-floor="1"]{--bg0:#0a130d;--bg1:#020302;--panel:#08100bdd;--panelEdge:#29442e;--gridEdge:#213929;--accent:#9fe2ab;--accent2:#f0cc76;--fog:#dbf5df;--mist:#88c6911c;--glow:#9be8a055;--gridGlow:#9be8a020;--floor:#132217;--memory:#667f6a;--wall:#6c786f;--pit:#ad584a;--exit:#f3d487;--torch:#ffbf67;--shrine:#9df1df;--relic:#ffe28f;--hazard:#ff8b77;--shade:#a7c8ff;--watcher:#ffd298;--leech:#8ef0db;--beast:#ff81d9;--player:#fbfff8}
45#s[data-floor="2"]{--bg0:#170d08;--bg1:#040202;--panel:#130b08e1;--panelEdge:#5c3520;--gridEdge:#4d2d1e;--accent:#ffb07a;--accent2:#ffd18f;--fog:#ffd9c0;--mist:#ff985415;--glow:#ff9d6260;--gridGlow:#ff9d6224;--floor:#24130d;--memory:#8a665b;--wall:#8f6d63;--pit:#dd6a55;--exit:#ffd48a;--torch:#ffaf59;--shrine:#ffd0a6;--relic:#ffe08e;--hazard:#ff7766;--shade:#d3c0ff;--watcher:#ffd0ad;--leech:#f1b68f;--beast:#ff7eb4;--player:#fff7f1}
46#s[data-floor="3"]{--bg0:#081416;--bg1:#020404;--panel:#071113e1;--panelEdge:#1c5459;--gridEdge:#1a474d;--accent:#8ce4eb;--accent2:#d8f09f;--fog:#d5fbff;--mist:#74f2ff18;--glow:#78ebf560;--gridGlow:#78ebf524;--floor:#0f2123;--memory:#628489;--wall:#6f8386;--pit:#b15a65;--exit:#dff2a3;--torch:#ffcf76;--shrine:#8ff8f1;--relic:#fff09c;--hazard:#ff8e86;--shade:#9fc8ff;--watcher:#f8eeae;--leech:#85f0dc;--beast:#ff9be4;--player:#f6ffff}
47#s[data-floor="4"]{--bg0:#170910;--bg1:#040203;--panel:#120810e1;--panelEdge:#5f1d39;--gridEdge:#4c1b31;--accent:#ff86b5;--accent2:#ffd39a;--fog:#ffd8e7;--mist:#ff6ca41a;--glow:#ff75b460;--gridGlow:#ff75b426;--floor:#22101a;--memory:#876071;--wall:#8c6a7a;--pit:#d75b64;--exit:#ffd392;--torch:#ffb262;--shrine:#f1a8ff;--relic:#ffe28b;--hazard:#ff7b7b;--shade:#b7b7ff;--watcher:#ffd2a6;--leech:#95f1d7;--beast:#ff5da8;--player:#fff7fb}
48#s[data-floor="5"]{--bg0:#0d0d10;--bg1:#010101;--panel:#0a0b0de1;--panelEdge:#3f4247;--gridEdge:#34373d;--accent:#c8d0da;--accent2:#f1d8a2;--fog:#e6ebf0;--mist:#ffffff10;--glow:#cbd6e245;--gridGlow:#cbd6e216;--floor:#17191d;--memory:#70767d;--wall:#838991;--pit:#c2645e;--exit:#f2ddb0;--torch:#e8b36f;--shrine:#c0ebff;--relic:#f5e2a7;--hazard:#ff9387;--shade:#aab7d9;--watcher:#ebd3a1;--leech:#a2dccf;--beast:#ff8ab4;--player:#fcfdff}
49#s[data-floor="6"]{--bg0:#140f08;--bg1:#030201;--panel:#110d08e2;--panelEdge:#6b5824;--gridEdge:#53461f;--accent:#f0d06d;--accent2:#fff0b3;--fog:#fff0c4;--mist:#f0c24a18;--glow:#f1cd6760;--gridGlow:#f1cd6726;--floor:#221d10;--memory:#8e7d52;--wall:#93886b;--pit:#d06d56;--exit:#fff0a6;--torch:#ffc768;--shrine:#d0f5c2;--relic:#fff1a1;--hazard:#ff8b67;--shade:#c7d6ff;--watcher:#ffe0a0;--leech:#beefc7;--beast:#ff7d9b;--player:#fffdf3}
50#s[data-floor="7"]{--bg0:#170c08;--bg1:#040201;--panel:#130a07e2;--panelEdge:#7a3f22;--gridEdge:#5e321c;--accent:#ff9a5c;--accent2:#ffd9a0;--fog:#ffe0b8;--mist:#ff8a4a1c;--glow:#ff9a5c60;--gridGlow:#ff9a5c26;--floor:#24140d;--memory:#93705a;--wall:#8f6f5c;--pit:#c9503f;--exit:#ffcf8f;--torch:#ffab5e;--shrine:#f2cfae;--relic:#ffe3a1;--hazard:#ff7a5c;--shade:#c9b6ff;--watcher:#ffcf8a;--leech:#c9e0a8;--beast:#ff6a7d;--player:#fff6ef}
51#s[data-state="title"]{--accent:#d9e6d6;--panelEdge:#445348;--gridEdge:#445348;--mist:#7fb48612}
52#s[data-state="dead"]{--accent:#ff8b79;--panelEdge:#6b2f27;--gridEdge:#612b24;--mist:#ff715812}
53#s[data-state="win"]{--accent:#f3dd8f;--panelEdge:#7a6930;--gridEdge:#6e5f2c;--mist:#f3dd8f18}
54#s[data-state="camp"]{--accent:#c8f2dd;--panelEdge:#376050;--gridEdge:#2f5648;--mist:#8ff0cc18}
55@media (max-width:780px){#s{justify-content:flex-start;padding:12px 10px 20px}#p{flex-direction:column;align-items:center;width:100%}#g,#i,#g.splash,#l,#u{width:min(92vw,420px);max-width:100%}#i,#g.splash{min-width:0}#g.splash{overflow:auto}#s[data-state="play"] #d{display:grid}}
56@keyframes flicker{0%,100%{opacity:1;transform:translateY(0)}25%{opacity:.75;transform:translateY(-.5px)}50%{opacity:.94}75%{opacity:.68;transform:translateY(.5px)}}
57@keyframes pulse{0%,100%{opacity:.84;transform:scale(1)}50%{opacity:1;transform:scale(1.08)}}
58@keyframes step{0%{transform:scale(.82)}100%{transform:scale(1)}}
59@keyframes wardbreak{0%{filter:brightness(1.35)}100%{filter:brightness(1)}}
60@keyframes relicpulse{0%{transform:scale(1)}50%{transform:scale(1.035)}100%{transform:scale(1)}}
61</style>
62<div id=s><div id=u></div><div id=p><div id=g></div><div id=i></div></div><div id=l></div><div id=d><button data-k=ArrowUp class=pu>^</button><button data-k=ArrowLeft class=pl>&lt;</button><button data-k=. class=pw>*</button><button data-k=ArrowRight class=pr>&gt;</button><button data-k=ArrowDown class=pd>v</button></div><button id=r hidden>restart</button></div>
63<script src=data.js></script>
64<script>
65const SEl=document.getElementById("s"),U=document.getElementById("u"),G=document.getElementById("g"),I=document.getElementById("i"),L=document.getElementById("l"),X=document.getElementById("r"),D=document.getElementById("d")
66let muted=0
67const SFX={
68 ctx:null,
69 beep(freq,dur,type){
70 if(muted||!freq)return
71 if(!this.ctx)this.ctx=new(window.AudioContext||window.webkitAudioContext)()
72 if(this.ctx.state=="suspended")this.ctx.resume()
73 const osc=this.ctx.createOscillator(),gain=this.ctx.createGain()
74 osc.type=type;osc.frequency.value=freq
75 gain.gain.setValueAtTime(.15,this.ctx.currentTime)
76 gain.gain.exponentialRampToValueAtTime(.0001,this.ctx.currentTime+dur)
77 osc.connect(gain);gain.connect(this.ctx.destination)
78 osc.start();osc.stop(this.ctx.currentTime+dur)
79 },
80 play(fx){
81 const s=SFX_MAP[fx]
82 if(s)this.beep(s.freq,s.dur,s.type)
83 }
84}
85let seed=0,B0=[10,8],R,S,V={hall:0,pit:0,idol:0,crack:0,escape:0,beast:0},DBG={show:0,out:""},O={relics:[],curses:[],shades:[],blocks:[],braziers:[],shrines:[],watchers:[],leeches:[],effigies:[]},hasSave=0
86
87function rng(n){
88 return function(){
89 n|=0;n=n+0x6D2B79F5|0
90 let t=Math.imul(n^n>>>15,1|n)
91 t=t+Math.imul(t^t>>>7,61|t)^t
92 return ((t^t>>>14)>>>0)/4294967296
93 }
94}
95function rollSeed(){return Math.floor(Math.random()*1e9)}
96function pickSpots(r,spots,n){
97 const bag=spots.slice(),out=[]
98 for(let i=0;i<n&&bag.length;i++)out.push(...bag.splice((r()*bag.length)|0,1))
99 return out
100}
101function pickMods(seed0){
102 const r=rng(seed0+404),bag=MOD_IDS.slice(),out=[]
103 while(out.length<2&&bag.length)out.push(bag.splice((r()*bag.length)|0,1)[0])
104 return out
105}
106function hash(s){let n=0;for(const ch of s)n=(n*31+ch.charCodeAt(0))|0;return Math.abs(n)}
107function modeMods(mode){
108 if(mode=="iron")return ["wake"]
109 if(mode=="greed")return ["rich"]
110 if(mode=="dark")return ["thin","gloom"]
111 if(mode=="warded")return ["grace"]
112 return []
113}
114function hasMod(id){return S.mods&&S.mods.includes(id)}
115function modSummary(){return S.mods.map(id=>MODS[id].label).join(", ")}
116function showHint(id,text=HINT_TEXT[id]){
117 if(!S.hints[id]){
118 S.hints[id]=1
119 S.hint=`hint ${text}`
120 }
121}
122function boolMark(v){return v?"✓":"-"}
123function statRow(k,v){return `${k.padEnd(7)} ${v}`}
124function loseWard(n){
125 const was=S.flags.ward||0
126 S.flags.ward=Math.max(0,was-n)
127 if(was&&!S.flags.ward)S.fx="ward"
128}
129function campOffers(next){
130 const r=rng(seed+next*313),pool=RELIC_IDS.filter(id=>id!=S.flags.relic),a=pool.splice((r()*pool.length)|0,1)[0],b=pool.splice((r()*pool.length)|0,1)[0]
131 return [a,b]
132}
133
134function build(seed0){
135 const L=LEVELS[S&&S.level||1],r=rng(seed0+((S&&S.level||1)-1)*9973)
136 const B=BASES[S&&S.level||1]
137 V={hall:(r()*L.halls.length)|0,pit:(r()*L.pits.length)|0,idol:(r()*L.idols.length)|0,crack:(r()*L.cracks.length)|0,escape:(r()*L.escapes.length)|0,beast:(r()*L.beasts.length)|0}
138 B0=L.beasts[V.beast]
139 const relicBag=RELIC_IDS.slice().sort(()=>r()-.5)
140 const level=S&&S.level||1
141 O={relics:pickSpots(r,RELIC_SPOTS[level]||[],(level>=5?RELIC_SLOTS_HIGH:level>=3?RELIC_SLOTS_MID:RELIC_SLOTS_LOW)+(S&&S.mods&&S.mods.includes("rich")?RELIC_SLOTS_RICH_BONUS:0)).map((v,i)=>({r:v[0],x:v[1],y:v[2],id:relicBag[i%relicBag.length],on:1})),
142 braziers:pickSpots(r,BRAZIER_SPOTS[level]||[],level>=5?3:level>=3?2:1).map(v=>({r:v[0],x:v[1],y:v[2],on:1})),
143 shrines:pickSpots(r,SHRINE_SPOTS[level]||[],level>=4?3:level>=3?2:1).map(v=>({r:v[0],x:v[1],y:v[2],on:1})),
144 curses:pickSpots(r,CURSE_SPOTS[level]||[],level==1?1:level>=5?3:2).map(v=>({r:v[0],x:v[1],y:v[2],on:1})),
145 shades:pickSpots(r,SHADE_SPOTS[level]||[],Math.min(level+1,5)).map(v=>({r:v[0],x:v[1],y:v[2],on:1})),
146 watchers:pickSpots(r,WATCHER_SPOTS[level]||[],Math.max(0,level-1)).map(v=>({r:v[0],x:v[1],y:v[2],on:1})),
147 leeches:pickSpots(r,LEECH_SPOTS[level]||[],Math.max(0,level-2)).map(v=>({r:v[0],x:v[1],y:v[2],on:1})),
148 blocks:pickSpots(r,BLOCK_SPOTS[level]||[],99).map(v=>({r:v[0],x:v[1],y:v[2],on:1})),
149 effigies:pickSpots(r,EFFIGY_SPOTS[level]||[],Math.max(0,level-1)).map(v=>({r:v[0],x:v[1],y:v[2],on:1}))}
150 R={
151 g:B.g,b:B.b,v:B.v,d:B.d,
152 h:{m:L.halls[V.hall],ex:{a:"p",b:"i",c:"c",d:"d",">":"g"}},
153 p:{m:L.pits[V.pit],ex:{">":"v"}},
154 i:{m:L.idols[V.idol],ex:{">":"h"}},
155 c:{m:L.cracks[V.crack],ex:{">":"v"}},
156 e:{m:L.escapes[V.escape],ex:{">":"g"}}
157 }
158}
159
160function buildTitle(){
161 const z=runStyle()
162 if(S.flags.relic=="chain"||S.path.shrines>2)return "Warded Pilgrim"
163 if(S.flags.gold&&z.route=="crack")return "Gold-Bound Runner"
164 if(S.flags.relic=="lure"||S.path.drops>1)return "Beast Angler"
165 if(S.flags.mask&&S.flags.relic=="veil")return "Veiled Diver"
166 if(S.flags.relic=="greave"||z.route=="pit")return "Pit Warden"
167 if(S.flags.relic=="hush"||S.mode=="dark")return "Hush Walker"
168 return runStyle().style.replace(/\b\w/g,m=>m.toUpperCase())
169}
170function enemyNote(kind){
171 const arr=ENEMY_TEXT[kind]
172 return arr[(seed+S.level+kind.length)%arr.length]
173}
174function flavor(kind){
175 const pool=kind=="shrine"?SHRINE_TEXT:kind=="brazier"?BRAZIER_TEXT:CURSE_TEXT
176 return pool[(seed+S.turn+S.level)%pool.length]
177}
178function floorLine(level=S.level){
179 const arr=FLOOR_TEXT[level]
180 return arr[(seed+level)%arr.length]
181}
182function finaleName(){
183 const idx=(hash((S.flags.relic||"plain")+runStyle().route+(S.flags.gold?"g":"n")+(S.flags.ward?"w":"d")))%FINALE_NAMES.length
184 return FINALE_NAMES[idx]
185}
186function openTitle(){
187 seed=0
188 S={gameState:"title",mode:"standard",currentMode:"standard",alive:1,win:0,level:1,currentLevel:1,msg:"",endLead:"",deathCause:"",mods:[],flags:{},path:{},seen:{},turn:0}
189 hasSave=!!loadSave()
190}
191function startRun(mode="standard",fresh=1){
192 const dailySeed=hash(new Date().toISOString().slice(0,10))
193 seed=mode=="daily"?dailySeed:(fresh||!seed?rollSeed():seed)
194 const picked=pickMods(seed),forced=modeMods(mode),mods=[...new Set([...picked,...forced])].slice(0,3)
195 S={level:1,currentLevel:1,mode,currentMode:mode}
196 S.mods=mods
197 build(seed)
198 S={seed,mode,currentMode:mode,mods:S.mods,level:1,currentLevel:1,gameState:"play",deathCause:"",endTitle:"",endLead:"",fx:"shift",r:"g",x:P.g[0],y:P.g[1],alive:1,win:0,turn:0,crownTurn:-1,chase:0,dread:0,msg:`Begin floor 1. ${floorLine(1)}`,hint:"",intro:1,hints:{torch:0,ward:0,crack:0,beast:0},flags:{idol:0,gold:mode=="greed",torch:mode=="heavy"?FUEL+8:0,ash:0,crown:0,key:0,mask:0,ward:mode=="warded"?4:0,relic:"",relicPlus:0},beast:{x:B0[0],y:B0[1]},drop:null,path:{pit:0,crack:0,drops:0,shrines:0,relics:0,shades:0,watchers:0,leeches:0,camps:0,levels:1,finale:0},camp:null,seen:{},end:"",finaleName:""}
199}
200function reset(fresh){
201 if(fresh)openTitle()
202 else startRun(S&&S.mode||"standard",0)
203}
204openTitle()
205
206function enterLevel(next){
207 const keep={torch:S.flags.torch,ash:S.flags.ash,mask:S.flags.mask,gold:S.flags.gold,ward:S.flags.ward,relic:S.flags.relic,relicPlus:S.flags.relicPlus}
208 const intro=S.intro,hints=S.hints
209 S.level=next
210 S.currentLevel=next
211 build(seed)
212 S.r="g";S.x=P.g[0];S.y=P.g[1];S.turn=0;S.crownTurn=-1;S.chase=0;S.dread=0
213 S.flags={idol:0,gold:keep.gold,torch:keep.torch,ash:keep.ash,crown:0,key:0,mask:keep.mask,ward:next>=3?keep.ward:0,relic:keep.relic,relicPlus:keep.relicPlus}
214 S.beast={x:B0[0],y:B0[1]}
215 S.drop=null
216 S.path={pit:0,crack:0,drops:S.path.drops,shrines:S.path.shrines,relics:S.path.relics,shades:S.path.shades,watchers:S.path.watchers,leeches:S.path.leeches,camps:S.path.camps,levels:next,finale:S.path.finale}
217 S.intro=intro
218 S.hints=hints
219 S.hint=""
220 S.fx="shift"
221 setMsg(`Enter floor ${next}. Title: ${buildTitle()}. ${floorLine(next)}`)
222}
223
224function startCamp(next){
225 S.gameState="camp"
226 S.path.camps++
227 S.camp={next,offer:campOffers(next)}
228 S.endTitle=`camp | floor ${S.level} cleared`
229 S.endLead=`Between floors, you can temper or trade your relic before Level ${next} (${LEVEL_ARCS[next]}).`
230 S.fx="shift"
231 saveGame()
232}
233
234function saveGame(){
235 if(S.gameState!="camp")return
236 try{localStorage.setItem("mrl_save",JSON.stringify({v:1,S}))}catch(e){}
237}
238function loadSave(){
239 try{const raw=localStorage.getItem("mrl_save");return raw?JSON.parse(raw):null}catch(e){return null}
240}
241function clearSave(){
242 try{localStorage.removeItem("mrl_save")}catch(e){}
243}
244function continueSave(){
245 const save=loadSave()
246 if(!save)return
247 S=save.S
248 seed=S.seed
249 build(seed)
250 S.gameState="camp"
251 hasSave=!!loadSave()
252 render()
253}
254
255function startFinale(){
256 const keep={torch:S.flags.torch,ash:S.flags.ash,mask:S.flags.mask,gold:S.flags.gold,ward:S.flags.ward,relic:S.flags.relic,relicPlus:S.flags.relicPlus}
257 S.level=6
258 S.currentLevel=6
259 S.path.finale=1
260 const fName=finaleName(),fOffset=FINALE_NAMES.indexOf(fName)
261 build(seed+909+fOffset*131)
262 S.gameState="play"
263 S.r="e";S.x=P.e[0];S.y=P.e[1];S.turn=0;S.crownTurn=0;S.chase=2;S.dread=0
264 S.flags={idol:1,gold:keep.gold,torch:Math.max(keep.torch,4),ash:keep.ash,crown:1,key:1,mask:keep.mask,ward:Math.max(keep.ward,hasMod("grace")?10:8),relic:keep.relic,relicPlus:keep.relicPlus}
265 S.beast={x:B0[0],y:B0[1]}
266 S.drop=null
267 S.finaleName=fName
268 S.hint=""
269 S.fx="shift"
270 setMsg(`${fName}. Title: ${buildTitle()}. Carry the crown through the last warded gate.`)
271}
272
273function startPostgame(){
274 const keep={torch:S.flags.torch,ash:S.flags.ash,mask:S.flags.mask,gold:S.flags.gold,ward:S.flags.ward,relic:S.flags.relic,relicPlus:S.flags.relicPlus}
275 S.level=7
276 S.currentLevel=7
277 build(seed)
278 S.gameState="play"
279 S.r="g";S.x=P.g[0];S.y=P.g[1];S.turn=0;S.crownTurn=-1;S.chase=0;S.dread=0
280 S.flags={idol:0,gold:keep.gold,torch:Math.max(keep.torch,4),ash:keep.ash,crown:0,key:0,mask:keep.mask,ward:keep.ward,relic:keep.relic,relicPlus:keep.relicPlus}
281 S.beast={x:B0[0],y:B0[1]}
282 S.drop=null
283 S.path={pit:0,crack:0,drops:S.path.drops,shrines:S.path.shrines,relics:S.path.relics,shades:S.path.shades,watchers:S.path.watchers,leeches:S.path.leeches,camps:S.path.camps,levels:7,finale:S.path.finale}
284 S.hint=""
285 S.fx="shift"
286 setMsg(`Enter floor 7. Title: ${buildTitle()}. ${floorLine(7)}`)
287}
288
289function applyCampChoice(choice){
290 if(!S.camp)return
291 if(choice=="1"){
292 if(S.flags.relic)S.flags.relicPlus=Math.min(2,S.flags.relicPlus+1)
293 else S.flags.relic=S.camp.offer[0]
294 }
295 if(choice=="2")S.flags.relic=S.camp.offer[0],S.flags.relicPlus=0
296 if(choice=="3")S.flags.relic=S.camp.offer[1],S.flags.relicPlus=0
297 if(choice=="4"){
298 S.flags.relic=""
299 S.flags.relicPlus=0
300 S.flags.torch=Math.max(S.flags.torch,6)
301 S.flags.ward=Math.max(S.flags.ward,6)
302 }
303 S.gameState="play"
304 enterLevel(S.camp.next)
305 S.camp=null
306}
307
308function base(x,y,r=S.r){return R[r].m[y][x]}
309function overAt(r,x,y,t){return O[t].find(o=>o.on&&o.r==r&&o.x==x&&o.y==y)}
310function roomKey(r=S.r){return `${S.level}:${r}`}
311function sight(){
312 if(!dark())return 99
313 let v=S.flags.torch?5:S.flags.ward?4:S.flags.mask?3:2
314 if(S.flags.relic=="ember")v++
315 if(S.flags.relic=="wick")v++
316 if(S.r=="e")v++
317 return v
318}
319function visible(x,y,r=S.r){
320 if(r!=S.r)return 0
321 if(!dark(r))return 1
322 return Math.abs(x-S.x)+Math.abs(y-S.y)<=sight()
323}
324function remember(){
325 const k=roomKey()
326 S.seen[k]||(S.seen[k]={})
327 for(let y=0;y<H;y++)for(let x=0;x<W;x++)if(visible(x,y))S.seen[k][`${x},${y}`]=1
328}
329function seen(x,y,r=S.r){
330 const k=`${S.level}:${r}`
331 return !!(S.seen[k]&&S.seen[k][`${x},${y}`])
332}
333function tile(x,y,r=S.r){
334 if(overAt(r,x,y,"blocks"))return "#"
335 if(S.drop&&S.drop.r==r&&S.drop.x==x&&S.drop.y==y&&S.drop.fuel)return "T"
336 if(overAt(r,x,y,"braziers"))return "F"
337 if(overAt(r,x,y,"shrines"))return "S"
338 if(overAt(r,x,y,"relics"))return "r"
339 if(overAt(r,x,y,"effigies"))return "r"
340 if(overAt(r,x,y,"shades"))return "n"
341 if(overAt(r,x,y,"watchers"))return "q"
342 if(overAt(r,x,y,"leeches"))return "l"
343 if(overAt(r,x,y,"curses"))return "z"
344 if(r=="b"&&S.flags.mask&&x==4&&y==2)return "."
345 if(r=="b"&&S.flags.torch&&x==5&&y==2)return "."
346 return base(x,y,r)
347}
348function setMsg(msg){S.msg=msg}
349function die(msg,cause=msg){S.alive=0;S.gameState="dead";S.deathCause=cause;S.endTitle="dead";S.endLead=msg;S.fx="hit";setMsg(msg);S.end=summary();clearSave();hasSave=!1}
350function win(){S.win=1;S.gameState="win";S.endTitle="dawn";S.endLead="You escape alive.";S.fx="win";setMsg("Dawn spills through the gate. You escape alive.");S.end=summary();clearSave();hasSave=!1}
351function sign(n){return n<0?-1:n>0?1:0}
352function dist(ax,ay,bx,by){return Math.abs(ax-bx)+Math.abs(ay-by)}
353function dark(r=S.r){return LEVELS[S.level].dark.includes(r)}
354function carriedFire(){return S.flags.torch&&!S.flags.mask}
355function fire(){return carriedFire()||(S.drop&&S.drop.r=="e"&&S.drop.fuel)}
356
357function runStyle(){
358 const route=S.path.pit>S.path.crack?"pit":S.path.crack>S.path.pit?"crack":"mixed"
359 let style="pilgrim"
360 if(S.flags.relic=="ember")style="ember runner"
361 else if(S.flags.relic=="tooth")style="greed runner"
362 else if(S.flags.relic=="hush")style="hush walker"
363 else if(S.flags.relic=="eye")style="idol sworn"
364 else if(S.flags.relic=="brand")style="shade breaker"
365 else if(S.flags.relic=="wick")style="longflame scout"
366 else if(S.flags.relic=="chain")style="ward binder"
367 else if(S.flags.relic=="veil")style="veiled diver"
368 else if(S.flags.relic=="greave")style="pit runner"
369 else if(S.flags.relic=="lure")style="beast angler"
370 else if(S.flags.relic=="salt")style="salt walker"
371 else if(S.flags.relic=="ash")style="brazier feeder"
372 else if(S.path.shrines)style="ward bearer"
373 else if(S.flags.mask&&route=="crack")style="mask runner"
374 else if(S.path.drops)style="torch tamer"
375 else if(S.flags.gold)style="gold bearer"
376 else if(route=="pit")style="warded crawl"
377 return {route,style}
378}
379
380function roomMsg(r=S.r){
381 if(r=="g")return S.flags.crown&&S.flags.key?(S.level==1?"The gate shudders instead of opening. Something deeper calls you on.":S.level<5?"The gate yields, but the campaign is not done with you yet.":S.level==5?"The gate opens onto the final road.": "The last gate waits. Only what survives the shrines goes home."):(S.level==1?"The broken gate yawns behind you. Bones left, beast right, ruin below.":S.level==2?"The gate is still shut. The deeper ruin waits below.":S.level==3?"The gate is quiet, but the shrines are not.":S.level==4?"The hunt floor waits below the gate, listening.":S.level==5?"The hollow floor waits below the gate, hungry for what remains.":"The final gate is quiet. Shrines and hunger ring it like judges.")
382 if(r=="b")return S.flags.torch?(S.level==1?"The bone room is dim now, only soot and old footprints.":S.level==2?"The bones are freshened by newer ash. Even the dead have been disturbed.":"The bones are carved with ward-scratches. Someone learned too late."):(S.level==1?"A torch and a funeral mask wait among the bones.":S.level==2?"A torch and a funeral mask wait among the bones, as if placed for your return.":"A torch, a mask, and an old shrine wait among the bones.")
383 if(r=="e"){
384 const b=beastState()
385 if(b=="calm")return carriedFire()?`Your torch keeps the beast at bay for now. ${enemyNote("beast")}`:`A one-eyed beast prowls here. Every step draws it closer. ${enemyNote("beast")}`
386 if(b=="alert")return S.flags.gold?`The beast is quickened by gold, but fire still buys you time. ${enemyNote("beast")}`:`The beast has your scent now. Fire buys you time, not safety. ${enemyNote("beast")}`
387 return "The beast is enraged. There is an opening, but no mercy."
388 }
389 if(r=="h")return S.level==1?"A long hall divides the ruin: pit left supports control; crack right rewards speed.":S.level==2?"The hall is tighter now. Every branch feels one step closer to the beast.":S.level==3?"The hall is strung with ward-shrines. Blessing and danger live in the same stone.":S.level==4?"The hall is full of watchers. Noise becomes pursuit here.":"The hall is bare and hollow. Every route feels like the wrong one."
390 if(r=="p")return carriedFire()?(S.level==1?"Torchlight steadies the pit's edge.":S.level==2?"Torchlight finds only slivers of safe stone over the pit.":S.level==3?"Wardlight softens the pit, but it drinks your flame.":S.level==4?"Torchlight catches every false edge in the hunt pit.":"The pit remembers every step. Safe footing is never free."):S.level==1?"The pit is blind and wide. Stay to the wall.":S.level==2?"The pit feels hungrier now. The dark is close on every side.":S.level==3?"The pit answers shrines, not fear. Empty hands are still not enough.":S.level==4?"The pit keeps no safe secrets once the flame is gone.":"The pit is the hollow floor's oldest lie."
391 if(r=="i")return S.flags.mask?(S.level==1?"The idol does not care for your mask, only your bow.":S.level==2?"The idol has learned your face beneath the mask. Bow anyway.":S.level==3?"The idol and the shrine answer each other. Even masked, you must bow.":"The idol sees through every build you've made. Bow anyway."):(S.level==1?"The idol waits in gold. Bow before you claim what it guards.":S.level==2?"The idol waits in harsher silence. Bow before you claim what it guards.":S.level==3?"The idol can steady a ward, but the crown will try to break it.":"The idol is colder now. Every blessing here feels conditional.")
392 if(r=="c")return S.flags.mask?(S.level==1?"The mask lets you slip the crack with more than bone and breath.":S.level==2?"The mask lets you slip the crack, but the stone still wants your flame.":S.level==3?"A live ward can force the crack open, but it will cost you.":"The crack listens for weakness. A mask only fools part of it."):(S.level==1?"The crack is narrow and mean. Quick to cross, but it steals flame.":S.level==2?"The crack runs longer now: quick, cruel, and eager to waste your time.":S.level==3?"The crack can be bought with ward or paid for with your body.":"The crack is now a wager: speed against whatever fire you still have.")
393 if(r=="v")return S.flags.crown?(S.level==1?"The vault is emptying into chaos. Take the key and flee.":S.level==2?"The vault is tighter, meaner, and already waking the beast.":S.level==3?"The vault hums against your ward. Take the key and run before the blessing breaks.":S.level==4?"The vault is alive with watchers. The theft is the easy part.":"The final vault sheds every comfort. Take the key and survive the reckoning."):(S.level==1?"In the vault, the crown gleams beside a small iron key.":S.level==2?"The same crown waits again, but the ruin holds it with a tighter grip.":S.level==3?"The crown waits beside old shrine-scratches. Someone once tried to survive this place.":S.level==4?"The crown waits in a vault built to wake pursuit.":"The crown waits at the bottom of the hollow run, brighter than mercy.")
394 return "You are dead. Press r to begin again."
395}
396
397function inv(){
398 const f=S.flags
399 return [
400 "inventory",
401 statRow("torch",f.torch?f.torch:f.ash?"burnt":"-"),
402 statRow("ward",f.ward||"-"),
403 statRow("relic",f.relic?RELICS[f.relic].label+(f.relicPlus?` +${f.relicPlus}`:""):"-"),
404 statRow("mask",boolMark(f.mask)),
405 statRow("idol",boolMark(f.idol)),
406 statRow("crown",boolMark(f.crown)),
407 statRow("key",boolMark(f.key)),
408 statRow("gold",boolMark(f.gold))
409 ].join("\n")
410}
411
412function summary(){
413 const z=runStyle()
414 const build=S.flags.relic?S.flags.relic:S.flags.mask?"mask":S.path.shrines?"ward":S.flags.gold?"gold":"plain"
415 return `seed ${S.seed.toString(36)} | level ${S.level} | arc ${LEVEL_ARCS[S.level]} | turns ${S.turn} | route ${z.route} | style ${z.style} | build ${build} | mods ${modSummary()} | torch ${S.flags.torch||0} | ward ${S.flags.ward||0} | gold ${S.flags.gold?"yes":"no"} | bowed ${S.flags.idol?"yes":"no"} | mask ${S.flags.mask?"yes":"no"} | relic ${S.flags.relic||"none"}${S.flags.relicPlus?`+${S.flags.relicPlus}`:""} | camps ${S.path.camps} | drops ${S.path.drops} | shrines ${S.path.shrines} | shades ${S.path.shades} | watchers ${S.path.watchers} | leeches ${S.path.leeches} | finale ${S.path.finale?"yes":"no"} | beast ${beastState()}`
416}
417
418function endIdentity(){
419 const z=runStyle()
420 return `${z.style} | ${z.route} route`
421}
422
423function endStats(){
424 const z=runStyle()
425 const build=S.flags.relic?`${RELICS[S.flags.relic].name}${S.flags.relicPlus?` +${S.flags.relicPlus}`:""}`:S.flags.mask?"funeral mask":S.path.shrines?"warded":"plain"
426 const encounters=[
427 S.path.shades?`shades ${S.path.shades}`:"",
428 S.path.watchers?`watchers ${S.path.watchers}`:"",
429 S.path.leeches?`leeches ${S.path.leeches}`:"",
430 S.path.drops?`drops ${S.path.drops}`:"",
431 S.path.shrines?`shrines ${S.path.shrines}`:"",
432 S.path.camps?`camps ${S.path.camps}`:"",
433 S.path.finale?`finale yes`:""
434 ].filter(Boolean)
435 const lines=[
436 "run",
437 "------",
438 `seed ${S.seed.toString(36)}`,
439 `level ${S.level} (${LEVEL_ARCS[S.level]})`,
440 `mode ${MODES[S.mode||"standard"].label}`,
441 `turns ${S.turn}`,
442 `style ${endIdentity()}`,
443 "",
444 "build",
445 "------",
446 `relic ${build}`,
447 `mods ${modSummary()}`,
448 `route ${z.route}`,
449 "",
450 "state",
451 "------",
452 `torch ${S.flags.torch||0}${S.flags.ash&&!S.flags.torch?" (burnt)":""}`,
453 `ward ${S.flags.ward||0}`,
454 `gold ${S.flags.gold?"yes":"no"}`,
455 `bowed ${S.flags.idol?"yes":"no"}`,
456 `mask ${S.flags.mask?"yes":"no"}`,
457 `beast ${beastState()}`
458 ]
459 if(encounters.length)lines.push("","encounters","----------",...encounters)
460 return lines.join("\n")
461}
462
463function sidePanel(){
464 const z=runStyle()
465 const th=FLOOR_THEMES[S.level]
466 return `${inv()}\n\nrun\n${statRow("title",buildTitle())}\n${statRow("route",z.route)}\n${statRow("style",z.style)}\n${statRow("floor",`${S.level} ${th.name}`)}\n${statRow("mood",th.tag)}\n${statRow("mode",MODES[S.mode||"standard"].label)}\n${statRow("mods",modSummary())}\n${statRow("turns",S.turn)}\n${statRow("beast",beastState())}`
467}
468
469function frame(lines,w=31){
470 const body=lines.map(line=>{
471 const s=(line||"").slice(0,w)
472 return `| ${s}${" ".repeat(w-s.length)} |`
473 })
474 return [`+${"-".repeat(w+2)}+`,...body,`+${"-".repeat(w+2)}+`].join("\n")
475}
476function floorTheme(level=S.level){return FLOOR_THEMES[level]||FLOOR_THEMES[1]}
477function tileGlyph(t,x,y,vis){
478 if(vis&&x==S.x&&y==S.y)return "@"
479 if(vis&&S.r=="e"&&x==S.beast.x&&y==S.beast.y)return "B"
480 if(!vis&&t=="r")return "*"
481 if(!vis&&".#".includes(t))return t=="#"?"#":"."
482 return t=="."?".":t=="#"?"":t=="O"?"o":t=="r"?"*":t=="z"?"^":t=="x"?"!":t=="n"?"~":t=="q"?"Q":t=="l"?"j":t=="S"?"Y":t=="F"?"&":t=="T"?"t":t=="g"?"$":t=="M"?"m":t
483}
484function fogClass(x,y,vis,mem){
485 if(!vis&&!mem)return "u"
486 if(!vis)return "m fog3"
487 const d=Math.abs(x-S.x)+Math.abs(y-S.y),s=sight()
488 return d<=Math.max(1,s-2)?"v fog1":d<=Math.max(2,s-1)?"v mid fog2":"v far fog3"
489}
490function splash(title,sub,lines,footer){
491 return frame([title,"",sub,...lines,"",footer].filter(v=>v!==undefined),31)
492}
493
494function variantSummary(){
495 return `hall ${V.hall} | pit ${V.pit} | idol ${V.idol} | crack ${V.crack} | beast ${V.beast} | escape ${V.escape} | mods ${S.mods.join("/")}`
496}
497
498function escapeSolve(level=S.level,escapeIdx=V.escape,beastIdx=V.beast,torch=6){
499 const map=LEVELS[level].escapes[escapeIdx],start=P.e,beast=LEVELS[level].beasts[beastIdx],q=[[{x:start[0],y:start[1],bx:beast[0],by:beast[1],torch,chase:0,ward:level==3?8:0},""]],seen=new Set()
500 while(q.length){
501 const [s,path]=q.shift(),k=[s.x,s.y,s.bx,s.by,s.torch,s.chase,s.ward].join()
502 if(seen.has(k))continue
503 seen.add(k)
504 for(const [dx,dy,ch] of [[1,0,"R"],[-1,0,"L"],[0,1,"D"],[0,-1,"U"]]){
505 const nx=s.x+dx,ny=s.y+dy,t=map[ny]&&map[ny][nx]
506 if(!t||t=="#")continue
507 if(t==">")return path+ch
508 let x=nx,y=ny,bx=s.bx,by=s.by,chase=s.chase,lit=s.torch>0,ward=s.ward
509 let left=Math.max(0,s.torch-((level==2||level==4||level==5)?2:1))
510 if(level>=3&&ward)left=Math.max(0,left-1)
511 let state=level>=6&&((!lit&&!ward)||chase>4)?"enraged":level>=6?"alert":level==5&&(!lit||chase>3)?"enraged":level==5?"alert":level==4&&(!lit||chase>4)?"enraged":level==4?"alert":level==3&&ward?"alert":level==2&&(!lit||chase>5)?"enraged":level==2?"alert":left?"alert":"enraged"
512 if(state=="alert"&&left&&!(chase++%2)){q.push([{x,y,bx,by,torch:left,chase,ward},path+ch]);continue}
513 for(let i=0;i<(state=="enraged"?2:1);i++){
514 if(Math.abs(x-bx)+Math.abs(y-by)<=1){bx=-1;break}
515 const ddx=x-bx,ddy=y-by,ax=Math.abs(ddx),ay=Math.abs(ddy)
516 let tries=ax>=ay?[[sign(ddx),0],[0,sign(ddy)]]:[[0,sign(ddy)],[sign(ddx),0]]
517 if(tries[0][0]||tries[0][1]){
518 const a=tries[0],b=tries[1]
519 const ca=cover(bx+a[0],by+a[1]),cb=cover(bx+b[0],by+b[1])
520 if(cb>ca)tries=[b,a]
521 }
522 for(const [mx,my] of tries){
523 const tx=bx+mx,ty=by+my
524 if((mx||my)&&map[ty]&&map[ty][tx]!="#"){bx=tx;by=ty;break}
525 }
526 if(Math.abs(x-bx)+Math.abs(y-by)<=1){bx=-1;break}
527 }
528 if(bx>=0)q.push([{x,y,bx,by,torch:left,chase,ward},path+ch])
529 }
530 }
531 return ""
532}
533
534function validateSeeds(n=24){
535 const keep=S
536 let bad=0,msg=[]
537 for(const level of [2,3,4,5,6])for(let i=0;i<n;i++){
538 const t={level,mods:keep.mods||pickMods(seed+i)}
539 S=t;build(seed+i)
540 const path=escapeSolve(level,V.escape,V.beast,level>=3?8:6)
541 if(!path){bad++;if(msg.length<6)msg.push(`l${level}:${(seed+i).toString(36)} e${V.escape} b${V.beast}`)}
542 }
543 S=keep
544 build(seed)
545 return `check ${n} seeds | ${bad?"bad "+bad+" "+msg.join(", "):"all clear"}`
546}
547
548function dropTorch(){
549 S.hint=""
550 if(S.flags.torch){
551 S.drop={r:S.r,x:S.x,y:S.y,fuel:S.flags.torch}
552 S.flags.torch=0
553 S.flags.ash=0
554 S.path.drops++
555 return setMsg("Set down the torch.")
556 }
557 if(S.drop&&S.drop.r==S.r&&S.drop.x==S.x&&S.drop.y==S.y&&S.drop.fuel){
558 S.flags.torch=S.drop.fuel
559 S.drop=null
560 return setMsg("Recover the torch.")
561 }
562}
563
564function beastState(){
565 if(hasMod("wake")&&S.r=="e"&&S.level<4)return "alert"
566 if(S.level==6&&S.r=="e"&&((!S.flags.ward&&!fire())||S.chase>4))return "enraged"
567 if(S.level==6&&S.r=="e")return "alert"
568 if(S.level==5&&S.r=="e"&&(!fire()||S.chase>3||S.path.shades>2))return "enraged"
569 if(S.level==5&&S.r=="e")return "alert"
570 if(S.level==4&&S.r=="e"&&(!fire()||S.chase>4||S.path.shades>1))return "enraged"
571 if(S.level==4&&S.r=="e")return "alert"
572 if(S.level==3&&S.flags.ward&&S.r=="e")return "alert"
573 if(S.level==3&&S.flags.ward&&S.flags.crown)return "alert"
574 if(S.flags.relic=="tooth"&&S.r=="e"&&S.chase>3)return "enraged"
575 if(S.level==2&&S.r=="e"&&(!fire()||S.chase>5))return "enraged"
576 if(S.level==2&&(S.r=="e"||S.flags.gold||S.flags.crown))return "alert"
577 if(S.flags.crown&&!fire())return "enraged"
578 if(S.flags.crown&&S.r=="e"&&S.chase>8)return "enraged"
579 if(S.flags.crown||S.flags.gold)return "alert"
580 return "calm"
581}
582
583function cover(x,y){
584 let n=0
585 for(const d of [[1,0],[-1,0],[0,1],[0,-1]])if(tile(x+d[0],y+d[1],"e")=="#")n++
586 return n
587}
588
589function beastTurn(){
590 if(S.r!="e"||!S.alive||S.win)return
591 const s=beastState()
592 if(s=="calm"&&fire())return
593 if(s=="alert"&&carriedFire()&&S.flags.relic!="hush"&&S.flags.relic!="lure"&&!(S.chase++%2))return
594 const lure=S.drop&&S.drop.r=="e"&&S.drop.fuel&&S.flags.relic!="ember"||S.flags.relic=="lure"&&S.drop&&S.drop.r=="e"&&S.drop.fuel
595 const tx=lure?S.drop.x:S.x,ty=lure?S.drop.y:S.y
596 const radius=s=="calm"?BEAST_CALM_RADIUS:99
597 const speed=s=="enraged"?BEAST_ENRAGED_SPEED:1
598 if(dist(S.x,S.y,S.beast.x,S.beast.y)<=1)return die("The beast is on you before you can breathe.")
599 if(dist(tx,ty,S.beast.x,S.beast.y)>radius)return
600 const bx=S.beast.x,by=S.beast.y
601 for(let i=0;i<speed;i++){
602 const dx=tx-S.beast.x,dy=ty-S.beast.y,ax=Math.abs(dx),ay=Math.abs(dy)
603 let tries=ax>=ay?[[sign(dx),0],[0,sign(dy)]]:[[0,sign(dy)],[sign(dx),0]]
604 if(tries[0][0]||tries[0][1]){
605 const a=tries[0],b=tries[1]
606 const ca=cover(S.beast.x+a[0],S.beast.y+a[1]),cb=cover(S.beast.x+b[0],S.beast.y+b[1])
607 if((s=="alert"||s=="enraged")&&cb>ca)tries=[b,a]
608 }
609 for(const [mx,my] of tries){
610 const nx=S.beast.x+mx,ny=S.beast.y+my
611 if((mx||my)&&tile(nx,ny,"e")!="#"){S.beast.x=nx;S.beast.y=ny;break}
612 }
613 if(lure&&S.beast.x==S.drop.x&&S.beast.y==S.drop.y){S.drop.fuel=0;setMsg("Beast stamps out the torch.");break}
614 if(dist(S.x,S.y,S.beast.x,S.beast.y)<=1)return die("The beast tears you open.")
615 }
616 if(bx!=S.beast.x||by!=S.beast.y)S.fx="beast"
617}
618
619function burnTorch(){
620 if(!S.alive||S.win||!dark())return
621 if(S.flags.torch){
622 let drain=(S.level==2||S.level>=4)&&"ec".includes(S.r)?TORCH_DRAIN_HAZARD:TORCH_DRAIN_BASE
623 if(S.level>=3&&S.flags.ward)drain++
624 if(hasMod("thin"))drain++
625 if(S.flags.relic=="wick")drain++
626 if(S.flags.relic=="ember")drain=Math.max(1,drain-1)
627 if(S.flags.relic=="eye"&&S.flags.crown)drain++
628 if(S.flags.relicPlus&&S.flags.relic=="ember")drain=Math.max(1,drain-S.flags.relicPlus)
629 if(!S.flags.gold||S.flags.relic=="chain"||S.turn%2){
630 S.flags.torch=Math.max(0,S.flags.torch-drain)
631 if(!S.flags.torch){S.flags.ash=1;if(!S.msg)setMsg("Torch gutters out.")}
632 }
633 }
634 if(S.drop&&S.drop.r==S.r&&S.drop.fuel){
635 let dropDrain=1
636 if(S.flags.relic=="wick")dropDrain=0
637 S.drop.fuel=Math.max(0,S.drop.fuel-dropDrain)
638 if(!S.drop.fuel&&!S.msg)setMsg("Dropped torch gutters out.")
639 }
640}
641
642function shadow(){
643 if(S.r=="e")return 0
644 if(S.level>=3&&S.flags.ward){
645 const wardLoss=hasMod("grace")||S.flags.relic=="chain"?0:1
646 if(wardLoss)loseWard(wardLoss)
647 if(S.dread)S.dread--
648 return 0
649 }
650 if(S.flags.relic=="hush"){if(S.dread)S.dread--;return 0}
651 if(S.flags.mask){if(S.dread)S.dread--;return 0}
652 if(dark()&&!S.flags.torch){
653 if(S.dread<(S.level==2?8:6))S.dread++
654 if(S.dread>(hasMod("gloom")?1:S.level==2?2:3)&&((S.turn+seed)&(hasMod("gloom")?1:S.level==2?2:3))==0)return 1
655 }else if(S.dread)S.dread--
656 return 0
657}
658
659function enter(r,x=P[r][0],y=P[r][1],msg){
660 S.hint=""
661 S.r=r;S.x=x;S.y=y
662 if(r=="e"){S.chase=0;if(S.flags.crown)S.beast={x:B0[0],y:B0[1]}}
663 if(r=="p")S.path.pit++
664 if(r=="c")S.path.crack++
665 if(r=="d")return die("The black arch keeps its promise.")
666 if(r=="e"&&dist(S.x,S.y,S.beast.x,S.beast.y)<=1)return die("The beast is on you before you can breathe.")
667 if(r=="c"&&S.flags.gold&&!S.flags.mask&&S.flags.relic!="tooth"){
668 if(S.level>=3&&S.flags.ward){loseWard(S.flags.relic=="greave"?4:3);msg="Ward parts the crack; the blessing thins."}
669 else return die("Weighted by treasure, you wedge fast in the crack.")
670 }
671 if(r=="c"&&S.flags.torch&&!(S.flags.relic=="veil"&&S.flags.mask)){
672 S.flags.torch=Math.max(0,S.flags.torch-(S.flags.relic=="greave"?2:S.flags.relic=="wick"?0:S.flags.torch))
673 S.flags.ash=!S.flags.torch?1:S.flags.ash
674 msg=S.flags.torch?"Crack strips the torch.":"Crack snuffs the torch."
675 }
676 if(r=="g"&&S.flags.crown&&S.flags.key){
677 if(S.level<5)return startCamp(S.level+1)
678 if(S.level==5)return startFinale()
679 return win()
680 }
681 if(r=="b"&&!S.flags.torch)showHint("torch")
682 if(r=="c")showHint("crack")
683 if(r=="e")showHint("beast")
684 setMsg(msg||roomMsg(r))
685}
686
687function pitSafe(x,y){
688 if(S.flags.relic=="greave")return 1
689 if(S.mode=="swift")return 1
690 if(S.flags.torch)return 1
691 for(const d of [[1,0],[-1,0],[0,1],[0,-1]])if(tile(x+d[0],y+d[1])=="#")return 1
692 return 0
693}
694
695function touch(t,x,y){
696 if(t=="O"&&!pitSafe(x,y))return die("You step into the dark and do not find the edge.")
697 if(S.r=="e"&&x==S.beast.x&&y==S.beast.y)return die("The beast tears you open.")
698 if(S.drop&&S.drop.r==S.r&&S.drop.x==x&&S.drop.y==y&&S.drop.fuel){S.flags.torch=S.drop.fuel;S.drop=null;S.flags.ash=0;return setMsg("Recover the torch.")}
699 if(t=="T"&&!S.flags.torch){S.flags.torch=FUEL;S.flags.ash=0;showHint("torch");setMsg("Take the torch.")}
700 if(t=="M"&&!S.flags.mask){S.flags.mask=1;setMsg("Take the funeral mask. Dark eases, but flame stops slowing the beast.")}
701 if(t=="S"){
702 const wardBase=S.level>=3?8:4
703 const wardGain=(hasMod("grace")?2:0)+(S.flags.relic=="eye"?1:0)+(S.flags.relic=="chain"?2+S.flags.relicPlus:0)-(S.flags.relic=="veil")
704 S.flags.ward=Math.max(S.flags.ward,wardBase+wardGain)
705 if(S.flags.relic=="ash"&&S.flags.torch){S.flags.torch=Math.max(0,S.flags.torch-1);if(!S.flags.torch)S.flags.ash=1}
706 S.path.shrines++
707 showHint("ward")
708 setMsg(S.level>=3?"Raise a ward. Flame will feed it.":"Raise a ward. It will hold for a while.")
709 }
710 if(t=="r"){
711 const eff=overAt(S.r,x,y,"effigies")
712 if(eff){eff.on=0;return die("The second relic was never real. Poison closes your throat.")}
713 const o=overAt(S.r,x,y,"relics")
714 if(o){
715 S.flags.relic=o.id;S.flags.relicPlus=0;S.path.relics++;o.on=0;S.fx="relic"
716 setMsg(`Claim ${RELICS[o.id].name}. Gain ${RELICS[o.id].up}; lose ${RELICS[o.id].down}.${S.path.relics==1?` Title: ${buildTitle()}.`:""}`)
717 }
718 }
719 if(t=="F"){
720 const cap=FUEL+(S.flags.relic=="wick"?4:0)
721 const fill=S.flags.relic=="ash"?10:S.flags.relic=="wick"?8:6
722 const gain=S.flags.relic=="ash"?6:4
723 if(S.flags.ash||!S.flags.torch){S.flags.torch=fill;S.flags.ash=0;setMsg("Light the torch from the brazier.")}
724 else if(S.flags.torch<cap+(S.flags.relic=="ash"?2:0)){S.flags.torch=Math.min(cap+(S.flags.relic=="ash"?2:0),S.flags.torch+gain);setMsg("Feed the torch at the brazier.")}
725 else setMsg("Leave the brazier. The torch is full.")
726 }
727 if(t=="x"){S.fx="hit";setMsg("Trigger the crack underfoot.");burnTorch();beastTurn();if(!S.alive)return}
728 if(t=="z"){
729 S.dread=Math.min(8,S.dread+(S.flags.relic=="brand"?3:S.flags.relic=="salt"?1:2))
730 if(S.flags.ward)loseWard(1)
731 if(S.flags.torch&&S.flags.relic=="brand"){S.flags.torch=Math.max(0,S.flags.torch-1);if(!S.flags.torch)S.flags.ash=1}
732 S.fx="hit"
733 setMsg("Wake the curse. Dread rises.")
734 }
735 if(t=="n"){
736 const o=overAt(S.r,x,y,"shades")
737 if(o)o.on=0
738 S.path.shades++
739 if(S.flags.relic=="brand"){S.flags.ward=Math.min(9,S.flags.ward+2);setMsg("Break the shade and steal its spite.");return}
740 S.dread=Math.min(8,S.dread+2)
741 if(S.flags.torch){S.flags.torch=Math.max(0,S.flags.torch-(S.flags.relic=="salt"?3:2));if(!S.flags.torch)S.flags.ash=1}
742 if(S.r=="e")S.chase+=2
743 S.fx="hit"
744 setMsg("Take the shade's chill. Beast pressure rises.")
745 }
746 if(t=="q"){
747 const o=overAt(S.r,x,y,"watchers")
748 if(o)o.on=0
749 S.path.watchers++
750 S.dread=Math.min(8,S.dread+(S.flags.relic=="salt"?0:1))
751 S.chase+=S.level>=4?3:2
752 if(S.flags.torch&&S.flags.relic!="salt"){S.flags.torch=Math.max(0,S.flags.torch-1);if(!S.flags.torch)S.flags.ash=1}
753 S.fx="hit"
754 setMsg("Wake a watcher. Beast pressure rises.")
755 }
756 if(t=="l"){
757 const o=overAt(S.r,x,y,"leeches")
758 if(o)o.on=0
759 S.path.leeches++
760 if(S.flags.ward)loseWard(S.flags.relic=="chain"?1:2)
761 if(S.flags.torch){S.flags.torch=Math.max(0,S.flags.torch-1);if(!S.flags.torch)S.flags.ash=1}
762 if(S.flags.gold)S.dread=Math.min(8,S.dread+1)
763 S.fx="hit"
764 setMsg("Feed the leech ward and flame.")
765 }
766 if((S.r=="i"&&!S.flags.idol&&(t=="I"||t=="g"||t=="S"))||(S.flags.relic=="eye"&&S.r=="i"&&!S.flags.idol)){S.flags.idol=1;if(S.level==3&&S.flags.ward)S.flags.ward=Math.min(9,S.flags.ward+2);setMsg("Bow to the idol.")}
767 if(t=="g"){S.flags.gold=1;if(!S.msg)setMsg("Take the gold. Beast pressure rises.")}
768 if(t=="C"){
769 if(!S.flags.idol)return die("You reach for the crown unbowed. The idol judges you.")
770 S.flags.crown=1
771 S.flags.gold=1
772 if(S.level==3&&S.flags.ward)loseWard(2)
773 if(S.flags.relic=="eye"){S.flags.torch=Math.max(0,S.flags.torch-3);if(!S.flags.torch)S.flags.ash=1}
774 S.crownTurn=S.turn
775 S.fx="hit"
776 setMsg("Lift the crown. The ruin wakes hungry.")
777 }
778 if(t=="K"&&!S.flags.key){S.flags.key=1;setMsg(S.flags.crown?"Take the key. Run for the gate.":"Take the key. The crown still waits.")}
779}
780
781function step(dx,dy){
782 if(S.gameState!="play")return
783 S.msg="";S.hint=""
784 const px=S.x,py=S.y
785 const nx=S.x+dx,ny=S.y+dy,t=tile(nx,ny)
786 if(t=="#")return setMsg(roomMsg())
787 S.turn++
788 if(shadow()){setMsg("Darkness twists the room around you.");burnTorch();beastTurn();return}
789 const ex=R[S.r].ex[t]
790 if(ex){
791 if(S.r=="e"&&nx==S.beast.x&&ny==S.beast.y)return die("The beast tears you open.")
792 burnTorch();return enter(ex)
793 }
794 S.x=nx;S.y=ny
795 if(px!=S.x||py!=S.y){S.fx="move";S.intro=0}
796 touch(t,nx,ny)
797 if(!S.alive||S.win)return
798 burnTorch()
799 beastTurn()
800 if(!S.alive||S.win)return
801 if(!S.msg)setMsg(roomMsg())
802}
803
804function wait(){
805 if(S.gameState!="play")return
806 S.hint=""
807 S.turn++
808 S.fx=""
809 if(shadow())S.msg="Freeze as dark closes in."
810 else S.msg="Wait one breath."
811 burnTorch()
812 beastTurn()
813 if(!S.alive||S.win)return
814}
815
816function jumpLevel(level=2){
817 startRun(S&&S.mode||"standard",0)
818 while(S.level<level)enterLevel(S.level+1)
819 render()
820}
821
822function key(e){
823 if(e.key=="m"){muted=!muted;render();return}
824 if(S.gameState=="win"&&(e.key=="p"||e.key=="7")){startPostgame();render();return}
825 if(S.gameState=="title"){
826 if("12345678".includes(e.key)){startRun(MODE_IDS[+e.key-1],1);render();return}
827 if(e.key=="r"){startRun("standard",1);render();return}
828 if(e.key=="c"&&hasSave){continueSave();return}
829 }
830 if(e.key=="r"){reset(1);render();return}
831 if(e.key=="R"){startRun(S&&S.mode||"standard",0);render();return}
832 if(e.key=="v"){DBG.show=!DBG.show;render();return}
833 if(e.key=="V"){DBG.out=validateSeeds();render();return}
834 if(e.key=="j"){jumpLevel(2);return}
835 if(e.key=="J"){jumpLevel(3);return}
836 if(e.key=="k"){jumpLevel(4);return}
837 if(e.key=="K"){jumpLevel(5);return}
838 if(S.gameState=="camp"){
839 if("1234".includes(e.key))applyCampChoice(e.key)
840 render()
841 return
842 }
843 if(S.gameState!="play"){render();return}
844 if(e.key=="n"){reset(1);render();return}
845 if(e.key=="t"){dropTorch();render();return}
846 if(e.key=="ArrowUp"||e.key=="w")step(0,-1)
847 if(e.key=="ArrowDown"||e.key=="s")step(0,1)
848 if(e.key=="ArrowLeft"||e.key=="a")step(-1,0)
849 if(e.key=="ArrowRight"||e.key=="d")step(1,0)
850 if(e.key==" "||e.key==".")wait()
851 render()
852}
853addEventListener("keydown",key)
854D.onclick=e=>{const k=e.target.dataset.k;if(k)key({key:k})}
855X.onclick=()=>{reset(1);render()}
856
857function draw(x,y){
858 const vis=visible(x,y),mem=seen(x,y)
859 if(!vis&&!mem)return ""
860 const t=tile(x,y)
861 return tileGlyph(t,x,y,vis)
862}
863
864function cls(x,y){
865 const vis=visible(x,y),mem=seen(x,y)
866 if(!vis&&!mem)return "u"
867 if(vis&&x==S.x&&y==S.y)return `p ${fogClass(x,y,vis,mem)} ${S.fx=="move"?"pm":""} ${S.flags.ward&&S.flags.ward<4?"warded":""}`.trim()
868 if(vis&&S.r=="e"&&x==S.beast.x&&y==S.beast.y)return `B ${fogClass(x,y,vis,mem)} ${S.fx=="beast"?"bm":""} ${beastState()}`.trim()
869 const t=tile(x,y),state=fogClass(x,y,vis,mem),near=S.flags.ward&&vis&&Math.abs(x-S.x)+Math.abs(y-S.y)<=1,kind=t=="#"?"w":t=="O"?"o":t=="."?"f":t==">"?"i":t
870 return `${kind} ${state} ${"Ozxnql".includes(t)?"haz":""} ${t=="T"?"drop":""} ${t=="S"||near?"warded":""} ${t=="S"&&S.flags.ward&&S.flags.ward<4?"stress":""}`.trim()
871}
872
873function render(){
874 U.className=S.gameState
875 SEl.dataset.state=S.gameState
876 SEl.dataset.floor=S.level||1
877 SEl.dataset.mode=S.mode||"standard"
878 if(S.gameState=="title"){
879 delete G.dataset.intro
880 U.innerHTML=`1MB ROGUELIKE<small>single-file campaign | minimalist descent</small>`
881 I.textContent=`modes\n1 standard ${MODES.standard.desc}\n2 iron ${MODES.iron.desc}\n3 greed ${MODES.greed.desc}\n4 dark ${MODES.dark.desc}\n5 daily ${MODES.daily.desc}\n6 heavy ${MODES.heavy.desc}\n7 swift ${MODES.swift.desc}\n8 warded ${MODES.warded.desc}\n\nkeys\n1-8 begin\nr quick run${hasSave?"\nc continue saved run":""}\nm mute sound\n\nglyphs\n@ you B beast * relic\nY shrine & brazier t torch\n! crack ^ curse Q watcher\nj leech $ gold > gate`
882 L.textContent=`Pick a mode, then descend. Flame buys time; wards buy mistakes.${hasSave?" A saved run is waiting — press c to continue.":""}`
883 X.textContent="start standard"
884 X.onclick=()=>{startRun("standard",1);render()}
885 X.hidden=false
886 G.className="splash shift"
887 G.textContent=splash("1MB ROGUELIKE","single-file campaign",[
888 "descend through six authored floors",
889 "carry light, ward, and stolen momentum",
890 "every room is text, timing, and pressure"
891 ],"press 1-5 or use the button below")
892 return
893 }
894 if(S.gameState=="camp"){
895 delete G.dataset.intro
896 const next=floorTheme(S.camp.next)
897 U.innerHTML=`CAMP<small>${buildTitle()} | floor ${S.level} to ${S.camp.next} | ${next.tag}</small>`
898 X.textContent="restart"
899 X.onclick=()=>{reset(1);render()}
900 I.textContent=`camp\n${statRow("title",buildTitle())}\n${statRow("next",`floor ${S.camp.next} ${next.name}`)}\n${statRow("accent",next.accent)}\n${statRow("fog",next.fog)}\n${statRow("mods",modSummary())}\n${statRow("relic",S.flags.relic?RELICS[S.flags.relic].name+(S.flags.relicPlus?` +${S.flags.relicPlus}`:""):"none")}\n\n1 temper current\n2 take ${RELICS[S.camp.offer[0]].label}\n3 take ${RELICS[S.camp.offer[1]].label}\n4 sacrifice`
901 L.textContent=`${S.endLead}\nTitle: ${buildTitle()}. Temper for consistency, trade for a pivot, or sacrifice for supplies.`
902 X.hidden=false
903 G.className="splash shift"
904 G.textContent=splash("CAMP","the ruin loosens its grip for one room",[
905 `next floor: ${S.camp.next} ${next.name}`,
906 `title : ${buildTitle()}`,
907 `accent : ${next.accent}`,
908 `fog : ${next.fog}`,
909 `offer : ${RELICS[S.camp.offer[0]].name}`,
910 `offer : ${RELICS[S.camp.offer[1]].name}`
911 ],"press 1-4 to choose")
912 return
913 }
914 if(S.gameState=="play"){
915 remember()
916 const th=floorTheme()
917 U.innerHTML=`floor ${S.level} | ${th.name}<small>${buildTitle()} | ${th.tag} | ${th.accent} | ${MODES[S.mode||"standard"].label} | seed ${S.seed.toString(36)}</small>`
918 I.textContent=sidePanel()+(DBG.show?`\n\ndebug\n${variantSummary()}\nsolve : ${escapeSolve(S.level,V.escape,V.beast,S.level>=3?8:6)?"ok":"fail"}${DBG.out?`\ncheck : ${DBG.out}`:""}\nkeys : v V R j J k K`:"")
919 L.textContent=[S.msg,S.hint,roomMsg()].filter(Boolean).join("\n")
920 X.onclick=()=>{reset(1);render()}
921 X.hidden=true
922 if(S.intro)G.dataset.intro=INTRO_TEXT
923 else delete G.dataset.intro
924 G.innerHTML=""
925 G.className=S.fx=="hit"?"hit":S.fx=="win"?"win":S.fx=="shift"?"shift":""
926 if(S.fx=="ward")G.className="ward"
927 if(S.fx=="relic")G.className="relic"
928 for(let y=0;y<H;y++)for(let x=0;x<W;x++){
929 const d=document.createElement("div")
930 d.className="t "+cls(x,y)
931 d.textContent=draw(x,y)
932 G.appendChild(d)
933 }
934 SFX.play(S.fx)
935 S.fx=""
936 return
937 }
938 const done=S.gameState=="dead",th=floorTheme()
939 delete G.dataset.intro
940 U.innerHTML=`${done?"DEAD":"ESCAPED"}<small>${done?S.deathCause:buildTitle()} | floor ${S.level} ${th.name} | seed ${S.seed.toString(36)}</small>`
941 X.textContent="restart"
942 X.onclick=()=>{reset(1);render()}
943 G.className=`splash ${S.gameState=="win"?"win":"hit"}`
944 SFX.play(done?"dead":"win")
945 G.textContent=splash(done?"RUN ENDED":"DAWN","the descent leaves a shape behind",[
946 `title : ${buildTitle()}`,
947 `route : ${runStyle().route}`,
948 `state : ${done?"broken":"survived"}`,
949 `floor : ${S.level} ${th.name}`,
950 `mods : ${modSummary()}`
951 ],done?"press restart to descend again":"the gate opens and the run resolves")
952 I.textContent=`summary\n${endStats()}${DBG.show?`\n\ndebug\n${variantSummary()}${DBG.out?`\ncheck: ${DBG.out}`:""}`:""}`
953 L.textContent=`${S.endLead}\n${done?"The ruin keeps the rest.":"The run closes cleanly at the gate."}${!done?"\npress p to descend further":""}`
954 X.hidden=false
955}
956render()
957</script>
README.md added +69
@@ -0,0 +1,69 @@
1# Micro-Roguelike
2
3Two tiny roguelikes, built at two very different size limits. Explore, grab loot, don't die, try to make it out.
4
5Pick one and open it in your browser — no install, no account, nothing to download but the file itself.
6
7---
8
9## 1MB Version — the full game
10
11**[Play it](https://rogue.zerolabs.sh/1mb/)**
12
13A proper dungeon crawl: a 12x12 grid, torches and shadows, a beast that hunts you, and six floors between you and daylight (plus a secret seventh, if you make it out alive).
14
15### Goal
16Reach the gate on floor 6 carrying the crown and the key. Survive long enough to use them.
17
18### Controls
19| Key | Action |
20|---|---|
21| Arrow keys / WASD | Move |
22| Space or `.` | Wait a turn |
23| `t` | Drop your torch |
24| `m` | Mute / unmute sound |
25| `r` | Restart |
26| `c` | Continue a saved run (from the title screen) |
27
28On mobile, an on-screen D-pad appears automatically.
29
30### The basics
31- **Torch** — keeps the dark at bay. It burns down over time and can be refueled at braziers.
32- **Ward** — a temporary shield. Shrines can raise one for you.
33- **Relics** — pick one up for a real edge, with a real cost. Only one at a time.
34- **Curses, shades, watchers, leeches** — the ruin's hazards. Learn to read them.
35- **The beast** — always out there, always listening. It gets faster the deeper you go.
36
37Between floors, you'll stop at a camp and choose an upgrade for the road ahead. Your progress is saved automatically at each camp, so you can pick up where you left off.
38
39### Game modes
40From the title screen, press a number to start:
41
421. **Standard** — the full campaign, as intended
432. **Iron** — the beast wakes up early
443. **Greed** — richer loot, steeper prices
454. **Dark** — a weaker flame and faster dread
465. **Daily** — everyone gets the same seed today
476. **Heavy** — start with a lit torch and extra fuel
487. **Swift** — pits can't catch you
498. **Warded** — start already shielded
50
51Win once, and a hidden seventh floor opens up — for anyone who isn't done yet.
52
53---
54
55## 1KB Version — the hypertext original
56
57**[Play it](https://rogue.zerolabs.sh/1kb/#g)**
58
59The whole game fits in about a kilobyte of HTML. No JavaScript — just links between rooms. Click your way through the ruin: find the torch, dodge the beast, don't reach for the wrong thing.
60
61### Controls
62Click the links. That's it.
63
64### Goal
65Find the key and the crown, and make it back out through the gate. Die, and you're one click from trying again.
66
67---
68
69*Both versions are free and open source, licensed under the GPLv3 — see [LICENSE](LICENSE).*