summaryrefslogtreecommitdiff
path: root/1mb
diff options
context:
space:
mode:
Diffstat (limited to '1mb')
-rw-r--r--1mb/data.js87
-rw-r--r--1mb/index.html227
2 files changed, 252 insertions, 62 deletions
diff --git a/1mb/data.js b/1mb/data.js
index 1a9e32f..2fdae59 100644
--- a/1mb/data.js
+++ b/1mb/data.js
@@ -944,7 +944,7 @@ const ESCAPES6=[[
"############"]]
const BEASTS6=[[10,8],[10,7],[9,7],[10,6]]
const LEVEL_ORDER=[1,2,3,4,5,6]
-const LEVEL_ARCS={1:"foundation",2:"pressure",3:"ward",4:"hunt",5:"hollow",6:"finale",7:"afterglow"}
+const LEVEL_ARCS={1:"foundation",2:"pressure",3:"ward",4:"hunt",5:"hollow",6:"finale",7:"afterglow",9:"proving ground"}
const MODES={
standard:{label:"standard",desc:"full campaign"},
iron:{label:"iron",desc:"early beast"},
@@ -980,7 +980,8 @@ const FLOOR_THEMES={
4:{name:"hunt",tag:"rose alarm",accent:"pink hunt",fog:"thin red haze"},
5:{name:"hollow",tag:"ash hush",accent:"bone gray",fog:"dry blackout"},
6:{name:"finale",tag:"crown weather",accent:"gold ruin",fog:"judging dark"},
- 7:{name:"afterglow",tag:"borrowed dawn",accent:"ash gold",fog:"embered haze"}
+ 7:{name:"afterglow",tag:"borrowed dawn",accent:"ash gold",fog:"embered haze"},
+ 9:{name:"proving ground",tag:"debug",accent:"bare stone",fog:"none"}
}
const ENEMY_TEXT={
beast:["A one-eyed pursuer that reads space better than fear.","It wants a lane, not a duel."],
@@ -1124,10 +1125,11 @@ const LEVELS={
4:{pits:PITS4,idols:IDOLS4,cracks:CRACKS4,halls:HALLS4,escapes:ESCAPES4,beasts:BEASTS4,dark:"behipcv",tone:"The hunt floor listens for every sound you make."},
5:{pits:PITS4,idols:IDOLS4,cracks:CRACKS4,halls:HALLS4,escapes:ESCAPES5,beasts:BEASTS5,dark:"behipcv",tone:"The hollow floor strips away whatever was merely enough."},
6:{pits:PITS4,idols:IDOLS4,cracks:CRACKS4,halls:HALLS4,escapes:ESCAPES6,beasts:BEASTS6,dark:"behipcv",tone:"The final gate wakes. Everything left in you will be tested."},
-7:{pits:PITS4,idols:IDOLS4,cracks:CRACKS4,halls:HALLS4,escapes:ESCAPES6,beasts:BEASTS6,dark:"behipcv",tone:"You already won. The ruin didn't agree to let you leave."}
+7:{pits:PITS4,idols:IDOLS4,cracks:CRACKS4,halls:HALLS4,escapes:ESCAPES6,beasts:BEASTS6,dark:"behipcv",tone:"You already won. The ruin didn't agree to let you leave."},
+9:{pits:PITS,idols:IDOLS,cracks:CRACKS,halls:HALLS,escapes:ESCAPES,beasts:BEASTS,dark:"",tone:"A proving ground outside the campaign."}
}
const BASE={
-g:{m:[
+g:[
"############",
"#..........#",
"#..L.....R.#",
@@ -1139,8 +1141,8 @@ g:{m:[
"#..........#",
"#..........#",
"#..........#",
-"############"],ex:{">":"h",L:"b",R:"e"}},
-b:{m:[
+"############"],
+b:[
"############",
"#..........#",
"#...MT.....#",
@@ -1152,8 +1154,8 @@ b:{m:[
"#..........#",
"#..........#",
"#..........#",
-"############"],ex:{">":"h"}},
-v:{m:[
+"############"],
+v:[
"############",
"#..........#",
"#....C.K...#",
@@ -1165,11 +1167,11 @@ v:{m:[
"#..........#",
"#....>.....#",
"#..........#",
-"############"],ex:{">":"e"}},
-d:{m:["############","#..........#","#..........#","#..........#","#....X.....#","#..........#","#..........#","#..........#","#..........#","#..........#","#..........#","############"],ex:{}}
+"############"],
+d:["############","#..........#","#..........#","#..........#","#....X.....#","#..........#","#..........#","#..........#","#..........#","#..........#","#..........#","############"]
}
const BASE2={
-g:{m:[
+g:[
"############",
"#..........#",
"#..L..#..R.#",
@@ -1181,8 +1183,8 @@ g:{m:[
"#..........#",
"#..........#",
"#..........#",
-"############"],ex:{">":"h",L:"b",R:"e"}},
-b:{m:[
+"############"],
+b:[
"############",
"#..........#",
"#...MT.....#",
@@ -1194,8 +1196,8 @@ b:{m:[
"#..........#",
"#..........#",
"#..........#",
-"############"],ex:{">":"h"}},
-v:{m:[
+"############"],
+v:[
"############",
"#..........#",
"#...#C.K...#",
@@ -1207,11 +1209,11 @@ v:{m:[
"#..........#",
"#...>......#",
"#..........#",
-"############"],ex:{">":"e"}},
+"############"],
d:BASE.d
}
const BASE3={
-g:{m:[
+g:[
"############",
"#..........#",
"#..L..S..R.#",
@@ -1223,8 +1225,8 @@ g:{m:[
"#..........#",
"#..........#",
"#..........#",
-"############"],ex:{">":"h",L:"b",R:"e"}},
-b:{m:[
+"############"],
+b:[
"############",
"#..........#",
"#..SMT.....#",
@@ -1236,8 +1238,8 @@ b:{m:[
"#..........#",
"#..........#",
"#..........#",
-"############"],ex:{">":"h"}},
-v:{m:[
+"############"],
+v:[
"############",
"#..........#",
"#...#C.K.S.#",
@@ -1249,7 +1251,46 @@ v:{m:[
"#..........#",
"#...>......#",
"#..........#",
-"############"],ex:{">":"e"}},
+"############"],
d:BASE.d
}
-const BASES={1:BASE,2:BASE2,3:BASE3,4:BASE3,5:BASE3,6:BASE3,7:BASE3}
+const BASES={1:BASE,2:BASE2,3:BASE3,4:BASE3,5:BASE3,6:BASE3,7:BASE3,9:BASE}
+// Procedural room slots: floors 2+ draw each variant pick from the authored
+// arrays above plus PROC_SLOTS generated layouts per room type (escapes only
+// through floor 5 — the finale arenas stay authored, as do BASES rooms).
+// Specs feed genRoom() in the engine: exits lists required exit letters
+// ("any" = random open tile), ring is the pit-room O border, walls and pits
+// are tile budgets for wall clusters and pit veins, traps/items sprinkle
+// chars onto open floor, stamp drops a small pattern whole onto the map.
+const PROC_SLOTS=2
+const PROC_SPECS={
+ pits:{ring:1,pits:9,walls:2,exits:{">":"any"},items:{F:1}},
+ idols:{walls:3,exits:{">":"any"},stamp:["ggg","gIg","ggg"]},
+ cracks:{walls:14,traps:{x:3},exits:{">":"any"}},
+ halls:{walls:5,exits:{a:"any",b:"any",c:"any",d:"any",">":"any"},items:{F:1}},
+ escapes:{walls:5,exits:{">":"any"}}
+}
+// Floor topology: room set + exit wiring, consumed generically by build().
+// base: fixed map from BASES[floor]. arr + v: variant array in LEVELS[floor]
+// and the V slot that picks the variant. An exit destination prefixed "!" is
+// one-way: it seals behind you after one crossing. Room codes beyond one char
+// (h2) are extra instances; they inherit family behavior from the first letter.
+const TOPO={
+ g:{base:"g",ex:{">":"h",L:"b",R:"e"}},
+ b:{base:"b",ex:{">":"h"}},
+ v:{base:"v",ex:{">":"e"}},
+ d:{base:"d",ex:{}},
+ h:{arr:"halls",v:"hall",ex:{a:"p",b:"i",c:"c",d:"d",">":"g"}},
+ p:{arr:"pits",v:"pit",ex:{">":"v"}},
+ i:{arr:"idols",v:"idol",ex:{">":"h"}},
+ c:{arr:"cracks",v:"crack",ex:{">":"v"}},
+ e:{arr:"escapes",v:"escape",ex:{">":"g"}}
+}
+const GRAPH={1:TOPO,2:TOPO,3:TOPO,4:TOPO,5:TOPO,6:TOPO,7:TOPO,
+// floor 9: debug proving ground (T in play) — a second hall instance reached
+// by a one-way drop; exit b in h2 climbs back to the first hall.
+9:{
+ g:{base:"g",ex:{">":"h"}},
+ h:{arr:"halls",v:"hall",ex:{a:"!h2",">":"g"}},
+ h2:{arr:"halls",v:"hall",ex:{b:"h",">":"g"}}
+}}
diff --git a/1mb/index.html b/1mb/index.html
index 32cc57c..4668483 100644
--- a/1mb/index.html
+++ b/1mb/index.html
@@ -131,13 +131,90 @@ function campOffers(next){
return [a,b]
}
+function genRoom(rand,spec){
+ const pick=n=>(rand()*n)|0,lo=spec.ring?2:1,hi=spec.ring?W-3:W-2
+ const bad=[...(spec.keep||[]),...(spec.blocked||[])]
+ const openAt=(g,x,y)=>g[y][x]=="."&&!bad.some(b=>b[0]==x&&b[1]==y)
+ 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}
+ for(let t=0;t<25;t++){
+ const sparse=t==24
+ const g=Array.from({length:H},(_,y)=>Array.from({length:W},(_,x)=>!x||!y||x==W-1||y==H-1?"#":"."))
+ 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"
+ if(!sparse){
+ for(let w=spec.walls||0;w>0;){
+ const hz=pick(2),len=2+pick(3)
+ const x=lo+pick(hi-lo+1-(hz?len:0)),y=lo+pick(hi-lo+1-(hz?0:len))
+ 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]="#"
+ w-=len
+ }
+ let px=lo+pick(hi-lo+1),py=lo+pick(hi-lo+1)
+ for(let p=spec.pits||0;p>0;p--){
+ if(openAt(g,px,py))g[py][px]="O"
+ if(pick(5)){const d=[[1,0],[-1,0],[0,1],[0,-1]][pick(4)]
+ px=Math.min(hi,Math.max(lo,px+d[0]));py=Math.min(hi,Math.max(lo,py+d[1]))
+ }else{px=lo+pick(hi-lo+1);py=lo+pick(hi-lo+1)}
+ }
+ }
+ if(spec.stamp){
+ const sh=spec.stamp.length,sw=spec.stamp[0].length
+ let put=0
+ for(let s=0;s<20&&!put;s++){
+ const sx=lo+pick(hi-lo+2-sw),sy=lo+pick(hi-lo+2-sh)
+ if(bad.some(b=>b[0]>=sx&&b[0]<sx+sw&&b[1]>=sy&&b[1]<sy+sh))continue
+ for(let y=0;y<sh;y++)for(let x=0;x<sw;x++)g[sy+y][sx+x]=spec.stamp[y][x]
+ put=1
+ }
+ if(!put)continue
+ }
+ let full=0
+ 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--){
+ const c=spots(g)
+ if(!c.length){full=1;break}
+ const [x,y]=c[pick(c.length)];g[y][x]=ch
+ }
+ for(const ch in spec.exits){
+ const at=spec.exits[ch]
+ if(at=="any"){
+ const c=spots(g)
+ if(!c.length){full=1;break}
+ const [x,y]=c[pick(c.length)];g[y][x]=ch
+ }else g[at[1]][at[0]]=ch
+ }
+ if(full)continue
+ for(const [x,y] of spec.keep||[])if(g[y][x]=="#"||g[y][x]=="O")g[y][x]="."
+ const solid=(x,y)=>g[y][x]=="#"||g[y][x]=="O"||(spec.blocked||[]).some(b=>b[0]==x&&b[1]==y)
+ let total=0,start=null
+ for(let y=0;y<H;y++)for(let x=0;x<W;x++)if(!solid(x,y)){total++;start=start||[x,y]}
+ const seen=new Set([start.join()]),q=[start]
+ while(q.length){
+ const [x,y]=q.pop()
+ for(const [dx,dy] of [[1,0],[-1,0],[0,1],[0,-1]]){
+ const nx=x+dx,ny=y+dy,k=nx+","+ny
+ if(nx<0||ny<0||nx>=W||ny>=H||solid(nx,ny)||seen.has(k))continue
+ seen.add(k);q.push([nx,ny])
+ }
+ }
+ if(seen.size==total)return g.map(row=>row.join(""))
+ }
+}
+function procSpec(kind,level,code){
+ const spec={...PROC_SPECS[kind]},keep=[(P[code]||P[code[0]]).slice()],blocked=[]
+ for(const s of BLOCK_SPOTS[level]||[])if(s[0]==code)blocked.push([s[1],s[2]])
+ for(const T of [RELIC_SPOTS,EFFIGY_SPOTS,BRAZIER_SPOTS,SHRINE_SPOTS,CURSE_SPOTS,SHADE_SPOTS,WATCHER_SPOTS,LEECH_SPOTS])
+ for(const s of T[level]||[])if(s[0]==code)keep.push([s[1],s[2]])
+ if(kind=="escapes")for(const b of LEVELS[level].beasts)keep.push(b.slice())
+ spec.keep=keep;spec.blocked=blocked
+ return spec
+}
+
function build(seed0){
- const L=LEVELS[S&&S.level||1],r=rng(seed0+((S&&S.level||1)-1)*9973)
- const B=BASES[S&&S.level||1]
- 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}
+ const level=S&&S.level||1
+ const L=LEVELS[level],r=rng(seed0+(level-1)*9973)
+ const B=BASES[level]
+ const slots=k=>level<2||(k=="escapes"&&level>=6)?0:PROC_SLOTS
+ 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}
B0=L.beasts[V.beast]
const relicBag=RELIC_IDS.slice().sort(()=>r()-.5)
- const level=S&&S.level||1
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})),
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})),
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){
leeches:pickSpots(r,LEECH_SPOTS[level]||[],Math.max(0,level-2)).map(v=>({r:v[0],x:v[1],y:v[2],on:1})),
blocks:pickSpots(r,BLOCK_SPOTS[level]||[],99).map(v=>({r:v[0],x:v[1],y:v[2],on:1})),
effigies:pickSpots(r,EFFIGY_SPOTS[level]||[],Math.max(0,level-1)).map(v=>({r:v[0],x:v[1],y:v[2],on:1}))}
- R={
- g:B.g,b:B.b,v:B.v,d:B.d,
- h:{m:L.halls[V.hall],ex:{a:"p",b:"i",c:"c",d:"d",">":"g"}},
- p:{m:L.pits[V.pit],ex:{">":"v"}},
- i:{m:L.idols[V.idol],ex:{">":"h"}},
- c:{m:L.cracks[V.crack],ex:{">":"v"}},
- e:{m:L.escapes[V.escape],ex:{">":"g"}}
+ R={}
+ for(const code in GRAPH[level]){
+ const spec=GRAPH[level][code],arr=L[spec.arr],vi=V[spec.v]
+ const m=spec.base?B[spec.base]:vi<arr.length?arr[vi]:genRoom(r,procSpec(spec.arr,level,code))||arr[vi%arr.length]
+ R[code]={m,ex:{...spec.ex}}
+ }
+ if(R.e&&V.escape>=L.escapes.length){
+ let ok=escapeSolve(level,V.escape,V.beast,level>=3?8:6)
+ for(let i=0;i<6&&!ok;i++){
+ R.e.m=genRoom(r,procSpec("escapes",level,"e"))||L.escapes[V.escape%L.escapes.length]
+ ok=escapeSolve(level,V.escape,V.beast,level>=3?8:6)
+ }
+ if(!ok)R.e.m=L.escapes[V.escape%L.escapes.length]
}
}
@@ -341,8 +424,8 @@ function tile(x,y,r=S.r){
if(overAt(r,x,y,"watchers"))return "q"
if(overAt(r,x,y,"leeches"))return "l"
if(overAt(r,x,y,"curses"))return "z"
- if(r=="b"&&S.flags.mask&&x==4&&y==2)return "."
- if(r=="b"&&S.flags.torch&&x==5&&y==2)return "."
+ if(r[0]=="b"&&S.flags.mask&&x==4&&y==2)return "."
+ if(r[0]=="b"&&S.flags.torch&&x==5&&y==2)return "."
return base(x,y,r)
}
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
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}
function sign(n){return n<0?-1:n>0?1:0}
function dist(ax,ay,bx,by){return Math.abs(ax-bx)+Math.abs(ay-by)}
-function dark(r=S.r){return LEVELS[S.level].dark.includes(r)}
+function dark(r=S.r){return LEVELS[S.level].dark.includes(r[0])}
function carriedFire(){return S.flags.torch&&!S.flags.mask}
function fire(){return carriedFire()||(S.drop&&S.drop.r=="e"&&S.drop.fuel)}
@@ -378,6 +461,7 @@ function runStyle(){
}
function roomMsg(r=S.r){
+ r=r[0]
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.")
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.")
if(r=="e"){
@@ -496,7 +580,7 @@ function variantSummary(){
}
function escapeSolve(level=S.level,escapeIdx=V.escape,beastIdx=V.beast,torch=6){
- 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()
+ 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()
while(q.length){
const [s,path]=q.shift(),k=[s.x,s.y,s.bx,s.by,s.torch,s.chase,s.ward].join()
if(seen.has(k))continue
@@ -531,18 +615,70 @@ function escapeSolve(level=S.level,escapeIdx=V.escape,beastIdx=V.beast,torch=6){
return ""
}
+function floorSolve(seed0,level=S&&S.level||1){
+ const kS=S,kV=V,kR=R,kO=O,kB=B0
+ S={level,mods:kS&&kS.mods||pickMods(seed0)}
+ build(seed0)
+ const solidAt=(rm,x,y)=>{const t=R[rm].m[y][x];return t=="#"||t=="O"||!!overAt(rm,x,y,"blocks")}
+ 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}
+ const exitTo=(rm,dest)=>{for(const ch in R[rm].ex)if(R[rm].ex[ch].replace("!","")==dest)return find(rm,ch);return null}
+ const cost=(rm,from,...tos)=>{
+ let total=0,at=from
+ for(const to of tos){
+ if(!to)return -1
+ let d=-1
+ const seen=new Set([at.join()]),q=[[at[0],at[1],0]]
+ while(q.length){
+ const [x,y,n]=q.shift()
+ if(x==to[0]&&y==to[1]){d=n;break}
+ for(const [dx,dy] of [[1,0],[-1,0],[0,1],[0,-1]]){
+ const nx=x+dx,ny=y+dy,k=nx+","+ny
+ if(nx<0||ny<0||nx>=W||ny>=H||seen.has(k)||solidAt(rm,nx,ny))continue
+ seen.add(k);q.push([nx,ny,n+1])
+ }
+ }
+ if(d<0)return -1
+ total+=d;at=to
+ }
+ return total
+ }
+ const sp=rm=>(P[rm]||P[rm[0]]).slice()
+ const out=(()=>{
+ if(!R.b||!R.h||!R.i||!R.v||!R.e)return ""
+ let total=0
+ for(const c of [
+ cost("g",sp("g"),exitTo("g","b")),
+ cost("b",sp("b"),find("b","T"),exitTo("b","h")),
+ cost("h",sp("h"),exitTo("h","i")),
+ cost("i",sp("i"),find("i","I"),exitTo("i","h"))
+ ]){if(c<0)return "";total+=c}
+ const via=r2=>{
+ const a=cost("h",sp("h"),exitTo("h",r2))
+ if(a<0)return -1
+ const b=cost(r2,sp(r2),exitTo(r2,"v"))
+ return b<0?-1:a+b
+ }
+ const vp=via("p"),vc=via("c")
+ if(vp<0&&vc<0)return ""
+ total+=vp<0?vc:vc<0?vp:Math.min(vp,vc)
+ const vault=cost("v",sp("v"),find("v","C"),find("v","K"),exitTo("v","e"))
+ if(vault<0)return ""
+ total+=vault
+ // floor 1 crosses e with fresh-run fuel plus a brazier refuel: 8 is fair
+ const esc=escapeSolve(level,V.escape,V.beast,level==2?6:8)
+ if(!esc)return ""
+ return "ok:"+(total+esc.length)
+ })()
+ S=kS;V=kV;R=kR;O=kO;B0=kB
+ return out
+}
+
function validateSeeds(n=24){
- const keep=S
let bad=0,msg=[]
- for(const level of [2,3,4,5,6])for(let i=0;i<n;i++){
- const t={level,mods:keep.mods||pickMods(seed+i)}
- S=t;build(seed+i)
- const path=escapeSolve(level,V.escape,V.beast,level>=3?8:6)
- if(!path){bad++;if(msg.length<6)msg.push(`l${level}:${(seed+i).toString(36)} e${V.escape} b${V.beast}`)}
+ for(const level of [1,2,3,4,5,6,7])for(let i=0;i<n;i++){
+ if(!floorSolve(seed+i,level)){bad++;if(msg.length<6)msg.push(`l${level}:${(seed+i).toString(36)}`)}
}
- S=keep
- build(seed)
- return `check ${n} seeds | ${bad?"bad "+bad+" "+msg.join(", "):"all clear"}`
+ return `check ${n} seeds x floors 1-7 | ${bad?"bad "+bad+" "+msg.join(", "):"all clear"}`
}
function dropTorch(){
@@ -582,6 +718,7 @@ function beastState(){
function cover(x,y){
let n=0
+ if(!R.e)return 0
for(const d of [[1,0],[-1,0],[0,1],[0,-1]])if(tile(x+d[0],y+d[1],"e")=="#")n++
return n
}
@@ -619,7 +756,7 @@ function beastTurn(){
function burnTorch(){
if(!S.alive||S.win||!dark())return
if(S.flags.torch){
- let drain=(S.level==2||S.level>=4)&&"ec".includes(S.r)?TORCH_DRAIN_HAZARD:TORCH_DRAIN_BASE
+ let drain=(S.level==2||S.level>=4)&&"ec".includes(S.r[0])?TORCH_DRAIN_HAZARD:TORCH_DRAIN_BASE
if(S.level>=3&&S.flags.ward)drain++
if(hasMod("thin"))drain++
if(S.flags.relic=="wick")drain++
@@ -656,31 +793,32 @@ function shadow(){
return 0
}
-function enter(r,x=P[r][0],y=P[r][1],msg){
+function enter(r,x=(P[r]||P[r[0]])[0],y=(P[r]||P[r[0]])[1],msg){
S.hint=""
S.r=r;S.x=x;S.y=y
- if(r=="e"){S.chase=0;if(S.flags.crown)S.beast={x:B0[0],y:B0[1]}}
- if(r=="p")S.path.pit++
- if(r=="c")S.path.crack++
- if(r=="d")return die("The black arch keeps its promise.")
- 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.")
- if(r=="c"&&S.flags.gold&&!S.flags.mask&&S.flags.relic!="tooth"){
+ const f=r[0]
+ if(f=="e"){S.chase=0;if(S.flags.crown)S.beast={x:B0[0],y:B0[1]}}
+ if(f=="p")S.path.pit++
+ if(f=="c")S.path.crack++
+ if(f=="d")return die("The black arch keeps its promise.")
+ 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.")
+ if(f=="c"&&S.flags.gold&&!S.flags.mask&&S.flags.relic!="tooth"){
if(S.level>=3&&S.flags.ward){loseWard(S.flags.relic=="greave"?4:3);msg="Ward parts the crack; the blessing thins."}
else return die("Weighted by treasure, you wedge fast in the crack.")
}
- if(r=="c"&&S.flags.torch&&!(S.flags.relic=="veil"&&S.flags.mask)){
+ if(f=="c"&&S.flags.torch&&!(S.flags.relic=="veil"&&S.flags.mask)){
S.flags.torch=Math.max(0,S.flags.torch-(S.flags.relic=="greave"?2:S.flags.relic=="wick"?0:S.flags.torch))
S.flags.ash=!S.flags.torch?1:S.flags.ash
msg=S.flags.torch?"Crack strips the torch.":"Crack snuffs the torch."
}
- if(r=="g"&&S.flags.crown&&S.flags.key){
+ if(f=="g"&&S.flags.crown&&S.flags.key){
if(S.level<5)return startCamp(S.level+1)
if(S.level==5)return startFinale()
return win()
}
- if(r=="b"&&!S.flags.torch)showHint("torch")
- if(r=="c")showHint("crack")
- if(r=="e")showHint("beast")
+ if(f=="b"&&!S.flags.torch)showHint("torch")
+ if(f=="c")showHint("crack")
+ if(f=="e")showHint("beast")
setMsg(msg||roomMsg(r))
}
@@ -786,8 +924,9 @@ function step(dx,dy){
if(t=="#")return setMsg(roomMsg())
S.turn++
if(shadow()){setMsg("Darkness twists the room around you.");burnTorch();beastTurn();return}
- const ex=R[S.r].ex[t]
+ let ex=R[S.r].ex[t]
if(ex){
+ if(ex[0]=="!"){delete R[S.r].ex[t];ex=ex.slice(1)}
if(S.r=="e"&&nx==S.beast.x&&ny==S.beast.y)return die("The beast tears you open.")
burnTorch();return enter(ex)
}
@@ -819,6 +958,15 @@ function jumpLevel(level=2){
render()
}
+function testFloor(){
+ startRun(S&&S.mode||"standard",0)
+ S.level=9;S.currentLevel=9
+ build(seed)
+ S.r="g";S.x=P.g[0];S.y=P.g[1]
+ setMsg("Proving ground. Exit a in the hall drops one-way into h2; exit b in h2 climbs back.")
+ render()
+}
+
function key(e){
if(e.key=="m"){muted=!muted;render();return}
if(S.gameState=="win"&&(e.key=="p"||e.key=="7")){startPostgame();render();return}
@@ -835,6 +983,7 @@ function key(e){
if(e.key=="J"){jumpLevel(3);return}
if(e.key=="k"){jumpLevel(4);return}
if(e.key=="K"){jumpLevel(5);return}
+ if(e.key=="T"){testFloor();return}
if(S.gameState=="camp"){
if("1234".includes(e.key))applyCampChoice(e.key)
render()
@@ -915,7 +1064,7 @@ function render(){
remember()
const th=floorTheme()
U.innerHTML=`floor ${S.level} | ${th.name}<small>${buildTitle()} | ${th.tag} | ${th.accent} | ${MODES[S.mode||"standard"].label} | seed ${S.seed.toString(36)}</small>`
- 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`:"")
+ 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`:"")
L.textContent=[S.msg,S.hint,roomMsg()].filter(Boolean).join("\n")
X.onclick=()=>{reset(1);render()}
X.hidden=true