| @@ -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> |