| @@ -131,13 +131,90 @@ function campOffers(next){ |
| 131 | return [a,b] |
131 | return [a,b] |
| 132 | } |
132 | } |
| 133 | |
133 | |
| |
134 | function genRoom(rand,spec){ |
| |
135 | const pick=n=>(rand()*n)|0,lo=spec.ring?2:1,hi=spec.ring?W-3:W-2 |
| |
136 | const bad=[...(spec.keep||[]),...(spec.blocked||[])] |
| |
137 | const openAt=(g,x,y)=>g[y][x]=="."&&!bad.some(b=>b[0]==x&&b[1]==y) |
| |
138 | const spots=g=>{const c=[];for(let y=lo;y<=hi;y++)for(let x=lo;x<=hi;x++)if(openAt(g,x,y))c.push([x,y]);return c} |
| |
139 | for(let t=0;t<25;t++){ |
| |
140 | const sparse=t==24 |
| |
141 | const g=Array.from({length:H},(_,y)=>Array.from({length:W},(_,x)=>!x||!y||x==W-1||y==H-1?"#":".")) |
| |
142 | if(spec.ring)for(let i=1;i<W-1;i++)g[1][i]=g[H-2][i]=g[i][1]=g[i][W-2]="O" |
| |
143 | if(!sparse){ |
| |
144 | for(let w=spec.walls||0;w>0;){ |
| |
145 | const hz=pick(2),len=2+pick(3) |
| |
146 | const x=lo+pick(hi-lo+1-(hz?len:0)),y=lo+pick(hi-lo+1-(hz?0:len)) |
| |
147 | for(let i=0;i<len;i++)if(openAt(g,hz?x+i:x,hz?y:y+i))g[hz?y:y+i][hz?x+i:x]="#" |
| |
148 | w-=len |
| |
149 | } |
| |
150 | let px=lo+pick(hi-lo+1),py=lo+pick(hi-lo+1) |
| |
151 | for(let p=spec.pits||0;p>0;p--){ |
| |
152 | if(openAt(g,px,py))g[py][px]="O" |
| |
153 | if(pick(5)){const d=[[1,0],[-1,0],[0,1],[0,-1]][pick(4)] |
| |
154 | px=Math.min(hi,Math.max(lo,px+d[0]));py=Math.min(hi,Math.max(lo,py+d[1])) |
| |
155 | }else{px=lo+pick(hi-lo+1);py=lo+pick(hi-lo+1)} |
| |
156 | } |
| |
157 | } |
| |
158 | if(spec.stamp){ |
| |
159 | const sh=spec.stamp.length,sw=spec.stamp[0].length |
| |
160 | let put=0 |
| |
161 | for(let s=0;s<20&&!put;s++){ |
| |
162 | const sx=lo+pick(hi-lo+2-sw),sy=lo+pick(hi-lo+2-sh) |
| |
163 | if(bad.some(b=>b[0]>=sx&&b[0]<sx+sw&&b[1]>=sy&&b[1]<sy+sh))continue |
| |
164 | for(let y=0;y<sh;y++)for(let x=0;x<sw;x++)g[sy+y][sx+x]=spec.stamp[y][x] |
| |
165 | put=1 |
| |
166 | } |
| |
167 | if(!put)continue |
| |
168 | } |
| |
169 | let full=0 |
| |
170 | for(const kind of [sparse?0:spec.traps,spec.items])if(kind)for(const ch in kind)for(let n=kind[ch];n>0;n--){ |
| |
171 | const c=spots(g) |
| |
172 | if(!c.length){full=1;break} |
| |
173 | const [x,y]=c[pick(c.length)];g[y][x]=ch |
| |
174 | } |
| |
175 | for(const ch in spec.exits){ |
| |
176 | const at=spec.exits[ch] |
| |
177 | if(at=="any"){ |
| |
178 | const c=spots(g) |
| |
179 | if(!c.length){full=1;break} |
| |
180 | const [x,y]=c[pick(c.length)];g[y][x]=ch |
| |
181 | }else g[at[1]][at[0]]=ch |
| |
182 | } |
| |
183 | if(full)continue |
| |
184 | for(const [x,y] of spec.keep||[])if(g[y][x]=="#"||g[y][x]=="O")g[y][x]="." |
| |
185 | const solid=(x,y)=>g[y][x]=="#"||g[y][x]=="O"||(spec.blocked||[]).some(b=>b[0]==x&&b[1]==y) |
| |
186 | let total=0,start=null |
| |
187 | for(let y=0;y<H;y++)for(let x=0;x<W;x++)if(!solid(x,y)){total++;start=start||[x,y]} |
| |
188 | const seen=new Set([start.join()]),q=[start] |
| |
189 | while(q.length){ |
| |
190 | const [x,y]=q.pop() |
| |
191 | for(const [dx,dy] of [[1,0],[-1,0],[0,1],[0,-1]]){ |
| |
192 | const nx=x+dx,ny=y+dy,k=nx+","+ny |
| |
193 | if(nx<0||ny<0||nx>=W||ny>=H||solid(nx,ny)||seen.has(k))continue |
| |
194 | seen.add(k);q.push([nx,ny]) |
| |
195 | } |
| |
196 | } |
| |
197 | if(seen.size==total)return g.map(row=>row.join("")) |
| |
198 | } |
| |
199 | } |
| |
200 | function procSpec(kind,level,code){ |
| |
201 | const spec={...PROC_SPECS[kind]},keep=[(P[code]||P[code[0]]).slice()],blocked=[] |
| |
202 | for(const s of BLOCK_SPOTS[level]||[])if(s[0]==code)blocked.push([s[1],s[2]]) |
| |
203 | for(const T of [RELIC_SPOTS,EFFIGY_SPOTS,BRAZIER_SPOTS,SHRINE_SPOTS,CURSE_SPOTS,SHADE_SPOTS,WATCHER_SPOTS,LEECH_SPOTS]) |
| |
204 | for(const s of T[level]||[])if(s[0]==code)keep.push([s[1],s[2]]) |
| |
205 | if(kind=="escapes")for(const b of LEVELS[level].beasts)keep.push(b.slice()) |
| |
206 | spec.keep=keep;spec.blocked=blocked |
| |
207 | return spec |
| |
208 | } |
| |
209 | |
| 134 | function build(seed0){ |
210 | function build(seed0){ |
| 135 | const L=LEVELS[S&&S.level||1],r=rng(seed0+((S&&S.level||1)-1)*9973) |
211 | const level=S&&S.level||1 |
| 136 | const B=BASES[S&&S.level||1] |
212 | const L=LEVELS[level],r=rng(seed0+(level-1)*9973) |
| 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} |
213 | const B=BASES[level] |
| |
214 | const slots=k=>level<2||(k=="escapes"&&level>=6)?0:PROC_SLOTS |
| |
215 | V={hall:(r()*(L.halls.length+slots("halls")))|0,pit:(r()*(L.pits.length+slots("pits")))|0,idol:(r()*(L.idols.length+slots("idols")))|0,crack:(r()*(L.cracks.length+slots("cracks")))|0,escape:(r()*(L.escapes.length+slots("escapes")))|0,beast:(r()*L.beasts.length)|0} |
| 138 | B0=L.beasts[V.beast] |
216 | B0=L.beasts[V.beast] |
| 139 | const relicBag=RELIC_IDS.slice().sort(()=>r()-.5) |
217 | 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})), |
218 | 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})), |
219 | 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})), |
220 | 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})), |
| @@ -147,13 +224,19 @@ function build(seed0){ |
| 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})), |
224 | 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})), |
225 | 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}))} |
226 | 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={ |
227 | R={} |
| 151 | g:B.g,b:B.b,v:B.v,d:B.d, |
228 | for(const code in GRAPH[level]){ |
| 152 | h:{m:L.halls[V.hall],ex:{a:"p",b:"i",c:"c",d:"d",">":"g"}}, |
229 | const spec=GRAPH[level][code],arr=L[spec.arr],vi=V[spec.v] |
| 153 | p:{m:L.pits[V.pit],ex:{">":"v"}}, |
230 | const m=spec.base?B[spec.base]:vi<arr.length?arr[vi]:genRoom(r,procSpec(spec.arr,level,code))||arr[vi%arr.length] |
| 154 | i:{m:L.idols[V.idol],ex:{">":"h"}}, |
231 | R[code]={m,ex:{...spec.ex}} |
| 155 | c:{m:L.cracks[V.crack],ex:{">":"v"}}, |
232 | } |
| 156 | e:{m:L.escapes[V.escape],ex:{">":"g"}} |
233 | if(R.e&&V.escape>=L.escapes.length){ |
| |
234 | let ok=escapeSolve(level,V.escape,V.beast,level>=3?8:6) |
| |
235 | for(let i=0;i<6&&!ok;i++){ |
| |
236 | R.e.m=genRoom(r,procSpec("escapes",level,"e"))||L.escapes[V.escape%L.escapes.length] |
| |
237 | ok=escapeSolve(level,V.escape,V.beast,level>=3?8:6) |
| |
238 | } |
| |
239 | if(!ok)R.e.m=L.escapes[V.escape%L.escapes.length] |
| 157 | } |
240 | } |
| 158 | } |
241 | } |
| 159 | |
242 | |
| @@ -341,8 +424,8 @@ function tile(x,y,r=S.r){ |
| 341 | if(overAt(r,x,y,"watchers"))return "q" |
424 | if(overAt(r,x,y,"watchers"))return "q" |
| 342 | if(overAt(r,x,y,"leeches"))return "l" |
425 | if(overAt(r,x,y,"leeches"))return "l" |
| 343 | if(overAt(r,x,y,"curses"))return "z" |
426 | if(overAt(r,x,y,"curses"))return "z" |
| 344 | if(r=="b"&&S.flags.mask&&x==4&&y==2)return "." |
427 | if(r[0]=="b"&&S.flags.mask&&x==4&&y==2)return "." |
| 345 | if(r=="b"&&S.flags.torch&&x==5&&y==2)return "." |
428 | if(r[0]=="b"&&S.flags.torch&&x==5&&y==2)return "." |
| 346 | return base(x,y,r) |
429 | return base(x,y,r) |
| 347 | } |
430 | } |
| 348 | function setMsg(msg){S.msg=msg} |
431 | function setMsg(msg){S.msg=msg} |
| @@ -350,7 +433,7 @@ function die(msg,cause=msg){S.alive=0;S.gameState="dead";S.deathCause=cause;S.en |
| 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} |
433 | 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} |
434 | 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)} |
435 | 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)} |
436 | function dark(r=S.r){return LEVELS[S.level].dark.includes(r[0])} |
| 354 | function carriedFire(){return S.flags.torch&&!S.flags.mask} |
437 | function carriedFire(){return S.flags.torch&&!S.flags.mask} |
| 355 | function fire(){return carriedFire()||(S.drop&&S.drop.r=="e"&&S.drop.fuel)} |
438 | function fire(){return carriedFire()||(S.drop&&S.drop.r=="e"&&S.drop.fuel)} |
| 356 | |
439 | |
| @@ -378,6 +461,7 @@ function runStyle(){ |
| 378 | } |
461 | } |
| 379 | |
462 | |
| 380 | function roomMsg(r=S.r){ |
463 | function roomMsg(r=S.r){ |
| |
464 | r=r[0] |
| 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.") |
465 | 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.") |
466 | 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"){ |
467 | if(r=="e"){ |
| @@ -496,7 +580,7 @@ function variantSummary(){ |
| 496 | } |
580 | } |
| 497 | |
581 | |
| 498 | function escapeSolve(level=S.level,escapeIdx=V.escape,beastIdx=V.beast,torch=6){ |
582 | 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() |
583 | const map=LEVELS[level].escapes[escapeIdx]||R.e.m,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){ |
584 | 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() |
585 | 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 |
586 | if(seen.has(k))continue |
| @@ -531,18 +615,70 @@ function escapeSolve(level=S.level,escapeIdx=V.escape,beastIdx=V.beast,torch=6){ |
| 531 | return "" |
615 | return "" |
| 532 | } |
616 | } |
| 533 | |
617 | |
| |
618 | function floorSolve(seed0,level=S&&S.level||1){ |
| |
619 | const kS=S,kV=V,kR=R,kO=O,kB=B0 |
| |
620 | S={level,mods:kS&&kS.mods||pickMods(seed0)} |
| |
621 | build(seed0) |
| |
622 | const solidAt=(rm,x,y)=>{const t=R[rm].m[y][x];return t=="#"||t=="O"||!!overAt(rm,x,y,"blocks")} |
| |
623 | const find=(rm,ch)=>{for(let y=0;y<H;y++)for(let x=0;x<W;x++)if(R[rm].m[y][x]==ch)return [x,y];return null} |
| |
624 | const exitTo=(rm,dest)=>{for(const ch in R[rm].ex)if(R[rm].ex[ch].replace("!","")==dest)return find(rm,ch);return null} |
| |
625 | const cost=(rm,from,...tos)=>{ |
| |
626 | let total=0,at=from |
| |
627 | for(const to of tos){ |
| |
628 | if(!to)return -1 |
| |
629 | let d=-1 |
| |
630 | const seen=new Set([at.join()]),q=[[at[0],at[1],0]] |
| |
631 | while(q.length){ |
| |
632 | const [x,y,n]=q.shift() |
| |
633 | if(x==to[0]&&y==to[1]){d=n;break} |
| |
634 | for(const [dx,dy] of [[1,0],[-1,0],[0,1],[0,-1]]){ |
| |
635 | const nx=x+dx,ny=y+dy,k=nx+","+ny |
| |
636 | if(nx<0||ny<0||nx>=W||ny>=H||seen.has(k)||solidAt(rm,nx,ny))continue |
| |
637 | seen.add(k);q.push([nx,ny,n+1]) |
| |
638 | } |
| |
639 | } |
| |
640 | if(d<0)return -1 |
| |
641 | total+=d;at=to |
| |
642 | } |
| |
643 | return total |
| |
644 | } |
| |
645 | const sp=rm=>(P[rm]||P[rm[0]]).slice() |
| |
646 | const out=(()=>{ |
| |
647 | if(!R.b||!R.h||!R.i||!R.v||!R.e)return "" |
| |
648 | let total=0 |
| |
649 | for(const c of [ |
| |
650 | cost("g",sp("g"),exitTo("g","b")), |
| |
651 | cost("b",sp("b"),find("b","T"),exitTo("b","h")), |
| |
652 | cost("h",sp("h"),exitTo("h","i")), |
| |
653 | cost("i",sp("i"),find("i","I"),exitTo("i","h")) |
| |
654 | ]){if(c<0)return "";total+=c} |
| |
655 | const via=r2=>{ |
| |
656 | const a=cost("h",sp("h"),exitTo("h",r2)) |
| |
657 | if(a<0)return -1 |
| |
658 | const b=cost(r2,sp(r2),exitTo(r2,"v")) |
| |
659 | return b<0?-1:a+b |
| |
660 | } |
| |
661 | const vp=via("p"),vc=via("c") |
| |
662 | if(vp<0&&vc<0)return "" |
| |
663 | total+=vp<0?vc:vc<0?vp:Math.min(vp,vc) |
| |
664 | const vault=cost("v",sp("v"),find("v","C"),find("v","K"),exitTo("v","e")) |
| |
665 | if(vault<0)return "" |
| |
666 | total+=vault |
| |
667 | // floor 1 crosses e with fresh-run fuel plus a brazier refuel: 8 is fair |
| |
668 | const esc=escapeSolve(level,V.escape,V.beast,level==2?6:8) |
| |
669 | if(!esc)return "" |
| |
670 | return "ok:"+(total+esc.length) |
| |
671 | })() |
| |
672 | S=kS;V=kV;R=kR;O=kO;B0=kB |
| |
673 | return out |
| |
674 | } |
| |
675 | |
| 534 | function validateSeeds(n=24){ |
676 | function validateSeeds(n=24){ |
| 535 | const keep=S |
| |
| 536 | let bad=0,msg=[] |
677 | let bad=0,msg=[] |
| 537 | for(const level of [2,3,4,5,6])for(let i=0;i<n;i++){ |
678 | for(const level of [1,2,3,4,5,6,7])for(let i=0;i<n;i++){ |
| 538 | const t={level,mods:keep.mods||pickMods(seed+i)} |
679 | if(!floorSolve(seed+i,level)){bad++;if(msg.length<6)msg.push(`l${level}:${(seed+i).toString(36)}`)} |
| 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 | } |
680 | } |
| 543 | S=keep |
681 | return `check ${n} seeds x floors 1-7 | ${bad?"bad "+bad+" "+msg.join(", "):"all clear"}` |
| 544 | build(seed) |
| |
| 545 | return `check ${n} seeds | ${bad?"bad "+bad+" "+msg.join(", "):"all clear"}` |
| |
| 546 | } |
682 | } |
| 547 | |
683 | |
| 548 | function dropTorch(){ |
684 | function dropTorch(){ |
| @@ -582,6 +718,7 @@ function beastState(){ |
| 582 | |
718 | |
| 583 | function cover(x,y){ |
719 | function cover(x,y){ |
| 584 | let n=0 |
720 | let n=0 |
| |
721 | if(!R.e)return 0 |
| 585 | for(const d of [[1,0],[-1,0],[0,1],[0,-1]])if(tile(x+d[0],y+d[1],"e")=="#")n++ |
722 | 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 |
723 | return n |
| 587 | } |
724 | } |
| @@ -619,7 +756,7 @@ function beastTurn(){ |
| 619 | function burnTorch(){ |
756 | function burnTorch(){ |
| 620 | if(!S.alive||S.win||!dark())return |
757 | if(!S.alive||S.win||!dark())return |
| 621 | if(S.flags.torch){ |
758 | if(S.flags.torch){ |
| 622 | let drain=(S.level==2||S.level>=4)&&"ec".includes(S.r)?TORCH_DRAIN_HAZARD:TORCH_DRAIN_BASE |
759 | let drain=(S.level==2||S.level>=4)&&"ec".includes(S.r[0])?TORCH_DRAIN_HAZARD:TORCH_DRAIN_BASE |
| 623 | if(S.level>=3&&S.flags.ward)drain++ |
760 | if(S.level>=3&&S.flags.ward)drain++ |
| 624 | if(hasMod("thin"))drain++ |
761 | if(hasMod("thin"))drain++ |
| 625 | if(S.flags.relic=="wick")drain++ |
762 | if(S.flags.relic=="wick")drain++ |
| @@ -656,31 +793,32 @@ function shadow(){ |
| 656 | return 0 |
793 | return 0 |
| 657 | } |
794 | } |
| 658 | |
795 | |
| 659 | function enter(r,x=P[r][0],y=P[r][1],msg){ |
796 | function enter(r,x=(P[r]||P[r[0]])[0],y=(P[r]||P[r[0]])[1],msg){ |
| 660 | S.hint="" |
797 | S.hint="" |
| 661 | S.r=r;S.x=x;S.y=y |
798 | 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]}} |
799 | const f=r[0] |
| 663 | if(r=="p")S.path.pit++ |
800 | if(f=="e"){S.chase=0;if(S.flags.crown)S.beast={x:B0[0],y:B0[1]}} |
| 664 | if(r=="c")S.path.crack++ |
801 | if(f=="p")S.path.pit++ |
| 665 | if(r=="d")return die("The black arch keeps its promise.") |
802 | if(f=="c")S.path.crack++ |
| 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.") |
803 | if(f=="d")return die("The black arch keeps its promise.") |
| 667 | if(r=="c"&&S.flags.gold&&!S.flags.mask&&S.flags.relic!="tooth"){ |
804 | if(f=="e"&&dist(S.x,S.y,S.beast.x,S.beast.y)<=1)return die("The beast is on you before you can breathe.") |
| |
805 | if(f=="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."} |
806 | 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.") |
807 | else return die("Weighted by treasure, you wedge fast in the crack.") |
| 670 | } |
808 | } |
| 671 | if(r=="c"&&S.flags.torch&&!(S.flags.relic=="veil"&&S.flags.mask)){ |
809 | if(f=="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)) |
810 | 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 |
811 | S.flags.ash=!S.flags.torch?1:S.flags.ash |
| 674 | msg=S.flags.torch?"Crack strips the torch.":"Crack snuffs the torch." |
812 | msg=S.flags.torch?"Crack strips the torch.":"Crack snuffs the torch." |
| 675 | } |
813 | } |
| 676 | if(r=="g"&&S.flags.crown&&S.flags.key){ |
814 | if(f=="g"&&S.flags.crown&&S.flags.key){ |
| 677 | if(S.level<5)return startCamp(S.level+1) |
815 | if(S.level<5)return startCamp(S.level+1) |
| 678 | if(S.level==5)return startFinale() |
816 | if(S.level==5)return startFinale() |
| 679 | return win() |
817 | return win() |
| 680 | } |
818 | } |
| 681 | if(r=="b"&&!S.flags.torch)showHint("torch") |
819 | if(f=="b"&&!S.flags.torch)showHint("torch") |
| 682 | if(r=="c")showHint("crack") |
820 | if(f=="c")showHint("crack") |
| 683 | if(r=="e")showHint("beast") |
821 | if(f=="e")showHint("beast") |
| 684 | setMsg(msg||roomMsg(r)) |
822 | setMsg(msg||roomMsg(r)) |
| 685 | } |
823 | } |
| 686 | |
824 | |
| @@ -786,8 +924,9 @@ function step(dx,dy){ |
| 786 | if(t=="#")return setMsg(roomMsg()) |
924 | if(t=="#")return setMsg(roomMsg()) |
| 787 | S.turn++ |
925 | S.turn++ |
| 788 | if(shadow()){setMsg("Darkness twists the room around you.");burnTorch();beastTurn();return} |
926 | if(shadow()){setMsg("Darkness twists the room around you.");burnTorch();beastTurn();return} |
| 789 | const ex=R[S.r].ex[t] |
927 | let ex=R[S.r].ex[t] |
| 790 | if(ex){ |
928 | if(ex){ |
| |
929 | if(ex[0]=="!"){delete R[S.r].ex[t];ex=ex.slice(1)} |
| 791 | if(S.r=="e"&&nx==S.beast.x&&ny==S.beast.y)return die("The beast tears you open.") |
930 | if(S.r=="e"&&nx==S.beast.x&&ny==S.beast.y)return die("The beast tears you open.") |
| 792 | burnTorch();return enter(ex) |
931 | burnTorch();return enter(ex) |
| 793 | } |
932 | } |
| @@ -819,6 +958,15 @@ function jumpLevel(level=2){ |
| 819 | render() |
958 | render() |
| 820 | } |
959 | } |
| 821 | |
960 | |
| |
961 | function testFloor(){ |
| |
962 | startRun(S&&S.mode||"standard",0) |
| |
963 | S.level=9;S.currentLevel=9 |
| |
964 | build(seed) |
| |
965 | S.r="g";S.x=P.g[0];S.y=P.g[1] |
| |
966 | setMsg("Proving ground. Exit a in the hall drops one-way into h2; exit b in h2 climbs back.") |
| |
967 | render() |
| |
968 | } |
| |
969 | |
| 822 | function key(e){ |
970 | function key(e){ |
| 823 | if(e.key=="m"){muted=!muted;render();return} |
971 | if(e.key=="m"){muted=!muted;render();return} |
| 824 | if(S.gameState=="win"&&(e.key=="p"||e.key=="7")){startPostgame();render();return} |
972 | if(S.gameState=="win"&&(e.key=="p"||e.key=="7")){startPostgame();render();return} |
| @@ -835,6 +983,7 @@ function key(e){ |
| 835 | if(e.key=="J"){jumpLevel(3);return} |
983 | if(e.key=="J"){jumpLevel(3);return} |
| 836 | if(e.key=="k"){jumpLevel(4);return} |
984 | if(e.key=="k"){jumpLevel(4);return} |
| 837 | if(e.key=="K"){jumpLevel(5);return} |
985 | if(e.key=="K"){jumpLevel(5);return} |
| |
986 | if(e.key=="T"){testFloor();return} |
| 838 | if(S.gameState=="camp"){ |
987 | if(S.gameState=="camp"){ |
| 839 | if("1234".includes(e.key))applyCampChoice(e.key) |
988 | if("1234".includes(e.key))applyCampChoice(e.key) |
| 840 | render() |
989 | render() |
| @@ -915,7 +1064,7 @@ function render(){ |
| 915 | remember() |
1064 | remember() |
| 916 | const th=floorTheme() |
1065 | 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>` |
1066 | 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`:"") |
1067 | I.textContent=sidePanel()+(DBG.show?`\n\ndebug\n${variantSummary()}\nsolve : ${R.e?escapeSolve(S.level,V.escape,V.beast,S.level>=3?8:6)?"ok":"fail":"n/a"}\nfloor : ${R.i&&R.v&&R.e?floorSolve(seed,S.level)||"fail":"n/a"}${DBG.out?`\ncheck : ${DBG.out}`:""}\nkeys : v V R j J k K T`:"") |
| 919 | L.textContent=[S.msg,S.hint,roomMsg()].filter(Boolean).join("\n") |
1068 | L.textContent=[S.msg,S.hint,roomMsg()].filter(Boolean).join("\n") |
| 920 | X.onclick=()=>{reset(1);render()} |
1069 | X.onclick=()=>{reset(1);render()} |
| 921 | X.hidden=true |
1070 | X.hidden=true |