| @@ -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} |
| |
4 | body{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: |
| |
6 | radial-gradient(circle at 50% 16%,var(--mist),transparent 30%), |
| |
7 | radial-gradient(circle at 50% 45%,var(--gridGlow),transparent 46%), |
| |
8 | linear-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: |
| |
14 | radial-gradient(circle at 50% 50%,color-mix(in srgb,var(--bg0) 72%,#0d1510) 0,var(--shadow) 58%,#000 88%), |
| |
15 | linear-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: |
| |
17 | radial-gradient(circle at 50% 50%,transparent 38%,var(--shadow) 76%), |
| |
18 | linear-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><</button><button data-k=. class=pw>*</button><button data-k=ArrowRight class=pr>></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> |
| |
65 | const 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") |
| |
66 | let muted=0 |
| |
67 | const 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 | } |
| |
85 | let 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 | |
| |
87 | function 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 | } |
| |
95 | function rollSeed(){return Math.floor(Math.random()*1e9)} |
| |
96 | function 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 | } |
| |
101 | function 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 | } |
| |
106 | function hash(s){let n=0;for(const ch of s)n=(n*31+ch.charCodeAt(0))|0;return Math.abs(n)} |
| |
107 | function 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 | } |
| |
114 | function hasMod(id){return S.mods&&S.mods.includes(id)} |
| |
115 | function modSummary(){return S.mods.map(id=>MODS[id].label).join(", ")} |
| |
116 | function showHint(id,text=HINT_TEXT[id]){ |
| |
117 | if(!S.hints[id]){ |
| |
118 | S.hints[id]=1 |
| |
119 | S.hint=`hint ${text}` |
| |
120 | } |
| |
121 | } |
| |
122 | function boolMark(v){return v?"✓":"-"} |
| |
123 | function statRow(k,v){return `${k.padEnd(7)} ${v}`} |
| |
124 | function 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 | } |
| |
129 | function 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 | |
| |
134 | function 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 | |
| |
160 | function 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 | } |
| |
170 | function enemyNote(kind){ |
| |
171 | const arr=ENEMY_TEXT[kind] |
| |
172 | return arr[(seed+S.level+kind.length)%arr.length] |
| |
173 | } |
| |
174 | function 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 | } |
| |
178 | function floorLine(level=S.level){ |
| |
179 | const arr=FLOOR_TEXT[level] |
| |
180 | return arr[(seed+level)%arr.length] |
| |
181 | } |
| |
182 | function 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 | } |
| |
186 | function 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 | } |
| |
191 | function 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 | } |
| |
200 | function reset(fresh){ |
| |
201 | if(fresh)openTitle() |
| |
202 | else startRun(S&&S.mode||"standard",0) |
| |
203 | } |
| |
204 | openTitle() |
| |
205 | |
| |
206 | function 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 | |
| |
224 | function 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 | |
| |
234 | function saveGame(){ |
| |
235 | if(S.gameState!="camp")return |
| |
236 | try{localStorage.setItem("mrl_save",JSON.stringify({v:1,S}))}catch(e){} |
| |
237 | } |
| |
238 | function loadSave(){ |
| |
239 | try{const raw=localStorage.getItem("mrl_save");return raw?JSON.parse(raw):null}catch(e){return null} |
| |
240 | } |
| |
241 | function clearSave(){ |
| |
242 | try{localStorage.removeItem("mrl_save")}catch(e){} |
| |
243 | } |
| |
244 | function 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 | |
| |
255 | function 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 | |
| |
273 | function 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 | |
| |
289 | function 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 | |
| |
308 | function base(x,y,r=S.r){return R[r].m[y][x]} |
| |
309 | function overAt(r,x,y,t){return O[t].find(o=>o.on&&o.r==r&&o.x==x&&o.y==y)} |
| |
310 | function roomKey(r=S.r){return `${S.level}:${r}`} |
| |
311 | function 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 | } |
| |
319 | function 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 | } |
| |
324 | function 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 | } |
| |
329 | function seen(x,y,r=S.r){ |
| |
330 | const k=`${S.level}:${r}` |
| |
331 | return !!(S.seen[k]&&S.seen[k][`${x},${y}`]) |
| |
332 | } |
| |
333 | function 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 | } |
| |
348 | function setMsg(msg){S.msg=msg} |
| |
349 | function 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} |
| |
350 | function 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} |
| |
351 | function sign(n){return n<0?-1:n>0?1:0} |
| |
352 | function dist(ax,ay,bx,by){return Math.abs(ax-bx)+Math.abs(ay-by)} |
| |
353 | function dark(r=S.r){return LEVELS[S.level].dark.includes(r)} |
| |
354 | function carriedFire(){return S.flags.torch&&!S.flags.mask} |
| |
355 | function fire(){return carriedFire()||(S.drop&&S.drop.r=="e"&&S.drop.fuel)} |
| |
356 | |
| |
357 | function 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 | |
| |
380 | function 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 | |
| |
397 | function 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 | |
| |
412 | function 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 | |
| |
418 | function endIdentity(){ |
| |
419 | const z=runStyle() |
| |
420 | return `${z.style} | ${z.route} route` |
| |
421 | } |
| |
422 | |
| |
423 | function 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 | |
| |
463 | function 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 | |
| |
469 | function 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 | } |
| |
476 | function floorTheme(level=S.level){return FLOOR_THEMES[level]||FLOOR_THEMES[1]} |
| |
477 | function 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 | } |
| |
484 | function 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 | } |
| |
490 | function splash(title,sub,lines,footer){ |
| |
491 | return frame([title,"",sub,...lines,"",footer].filter(v=>v!==undefined),31) |
| |
492 | } |
| |
493 | |
| |
494 | function 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 | |
| |
498 | function 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 | |
| |
534 | function 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 | |
| |
548 | function 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 | |
| |
564 | function 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 | |
| |
583 | function 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 | |
| |
589 | function 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 | |
| |
619 | function 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 | |
| |
642 | function 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 | |
| |
659 | function 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 | |
| |
687 | function 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 | |
| |
695 | function 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 | |
| |
781 | function 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 | |
| |
804 | function 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 | |
| |
816 | function jumpLevel(level=2){ |
| |
817 | startRun(S&&S.mode||"standard",0) |
| |
818 | while(S.level<level)enterLevel(S.level+1) |
| |
819 | render() |
| |
820 | } |
| |
821 | |
| |
822 | function 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 | } |
| |
853 | addEventListener("keydown",key) |
| |
854 | D.onclick=e=>{const k=e.target.dataset.k;if(k)key({key:k})} |
| |
855 | X.onclick=()=>{reset(1);render()} |
| |
856 | |
| |
857 | function 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 | |
| |
864 | function 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 | |
| |
873 | function 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 | } |
| |
956 | render() |
| |
957 | </script> |