Commit 0c7885650f

0c7885650f01525e22282e237dc543bfb82b8cac

parent: cb83ff3077

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-27 06:17 UTC

Explain every interactable and exit in the side panel

Nothing in a level said what most tiles did before touching them, and
five kill on contact or entry: door d (black arch), effigies, unsafe
pit edges, the crown before bowing, and the crack while carrying gold.

The side panel now opens with:
- here: one LEGEND line per interactable seen in the room, the effigy
  warning when fake relics are present, and the dark-room rule while
  torchless
- exits: every exit with its ROOM_NAMES destination, "(one way)", and
  state-dependent notes: d (death), c (gold: death / costs ward), gate
  (crown and key: go on)
- keys: t drop torch, . wait

In graphics mode each line carries its sprite; door d gets its own
black-arch sprite. The title screen lists g and points to the panel.

validate.js builds floors 1-7 for every seed and fails on any tile,
effigy set, or exit destination without a LEGEND or ROOM_NAMES entry
(room d is skipped: entering it kills).

Layout: unified · split

1mb/data.js +22
@@ -1022,6 +1022,28 @@ const HINT_TEXT={
1022 crack:"Cracks are fast but costly. Mask or ward makes the crossing safer.", 1022 crack:"Cracks are fast but costly. Mask or ward makes the crossing safer.",
1023 beast:"Fire slows pursuit. A dropped torch can buy one clean lane." 1023 beast:"Fire slows pursuit. A dropped torch can buy one clean lane."
1024} 1024}
1025// Side-panel legend: one line per interactable visible in the current room.
1026const LEGEND={
1027 O:"pit edge: kills unless beside a wall or carrying a torch",
1028 T:"torch: light; stops lost steps in the dark, keeps pit edges safe, slows the beast",
1029 M:"mask: eases dark, lets gold through the crack; fire stops slowing the beast",
1030 S:"shrine: raises ward; from floor 3 ward stops the dark stealing steps",
1031 r:"relic: a trade, one edge for one cost (named on pickup)",
1032 effigy:"some relics in this room are false; a false one kills",
1033 F:"brazier: relights or refuels your torch",
1034 x:"crack: burns torch and gives the beast a free move",
1035 z:"curse: raises dread (dark steals more steps) and chips ward",
1036 n:"shade: drains torch and raises dread; in the den it draws the beast",
1037 q:"watcher: beast pursuit jumps; costs torch",
1038 l:"leech: eats ward and torch",
1039 I:"idol: step on the idol, its gold, or its shrine to bow",
1040 g:"gold: quickens the beast; fatal in the crack unless masked",
1041 C:"crown: bow to the idol first or die; counts as gold",
1042 K:"key: take it with the crown back to the gate",
1043 B:"beast: kills on contact; fire and a dropped torch slow it",
1044 dark:"dark: without a torch, dread builds and steps get lost"
1045}
1046const ROOM_NAMES={g:"gate",b:"bone room",e:"beast den",h:"hall",p:"pit room",i:"idol room",c:"crack",v:"vault",d:"black arch"}
1025const RELICS={ 1047const RELICS={
1026 ember:{name:"ember heart",label:"ember",up:"slower torch drain",down:"beast ignores dropped flame"}, 1048 ember:{name:"ember heart",label:"ember",up:"slower torch drain",down:"beast ignores dropped flame"},
1027 tooth:{name:"gold tooth",label:"tooth",up:"gold slips the crack",down:"beast enrages sooner"}, 1049 tooth:{name:"gold tooth",label:"tooth",up:"gold slips the crack",down:"beast enrages sooner"},
1mb/index.html +32 −3
@@ -53,7 +53,7 @@ linear-gradient(180deg,transparent,rgba(0,0,0,.12));mix-blend-mode:screen;opacit
53#s[data-state="win"]{--accent:#f3dd8f;--panelEdge:#7a6930;--gridEdge:#6e5f2c;--mist:#f3dd8f18} 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} 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}} 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#s.gfx{--cs:min(40px,calc((92vw - 26px)/12))}#s.gfx #g:not(.splash){grid-template-columns:repeat(12,var(--cs));gap:0}#s.gfx .t{width:var(--cs);height:var(--cs);font-size:0;text-shadow:none}.t>svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}.t>b{position:relative;font-size:calc(var(--cs)*.6)}.t>b.dl{font-size:calc(var(--cs)*.34);color:var(--exit);top:10%}.B.alert{--eye:#ffa8e4}.B.enraged{--eye:#ff4040} 56#s.gfx{--cs:min(40px,calc((92vw - 26px)/12))}#s.gfx #g:not(.splash){grid-template-columns:repeat(12,var(--cs));gap:0}#s.gfx .t{width:var(--cs);height:var(--cs);font-size:0;text-shadow:none}.t>svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}.t>b{position:relative;font-size:calc(var(--cs)*.6)}.t>b.dl{font-size:calc(var(--cs)*.34);color:var(--exit);top:10%}.lg{white-space:normal;margin-bottom:10px;color:var(--muted)}.lr{display:flex;gap:6px;align-items:flex-start;color:var(--text);margin:1px 0}.lr i{flex:0 0 1.5em;height:1.5em;font-style:normal;text-align:center;color:var(--accent)}.lr i svg{width:100%;height:100%}.B.alert{--eye:#ffa8e4}.B.enraged{--eye:#ff4040}
57@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 flicker{0%,100%{opacity:1;transform:translateY(0)}25%{opacity:.75;transform:translateY(-.5px)}50%{opacity:.94}75%{opacity:.68;transform:translateY(.5px)}}
58@keyframes pulse{0%,100%{opacity:.84;transform:scale(1)}50%{opacity:1;transform:scale(1.08)}} 58@keyframes pulse{0%,100%{opacity:.84;transform:scale(1)}50%{opacity:1;transform:scale(1.08)}}
59@keyframes step{0%{transform:scale(.82)}100%{transform:scale(1)}} 59@keyframes step{0%{transform:scale(.82)}100%{transform:scale(1)}}
@@ -553,6 +553,34 @@ function sidePanel(){
553 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())}` 553 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())}`
554} 554}
555 555
556function exitNote(dest){
557 if(dest[0]=="d")return " (death)"
558 if(dest[0]=="c"&&S.flags.gold&&!S.flags.mask&&S.flags.relic!="tooth")return S.level>=3&&S.flags.ward?" (gold: costs ward)":" (gold: death)"
559 if(dest[0]=="g"&&S.flags.crown&&S.flags.key)return " (crown and key: go on)"
560 return ""
561}
562function legendRows(){
563 const here={},rows=[]
564 for(let y=0;y<H;y++)for(let x=0;x<W;x++)if(visible(x,y)||seen(x,y))here[tile(x,y)]=1
565 if(S.r[0]=="e"&&visible(S.beast.x,S.beast.y))here.B=1
566 if(here.r&&O.effigies.some(o=>o.on&&o.r==S.r))here.effigy=1
567 if(dark()&&!S.flags.torch)here.dark=1
568 for(const k in LEGEND)if(here[k])rows.push([k,LEGEND[k]])
569 return rows
570}
571function exitRows(){
572 const ex=R[S.r].ex
573 return Object.keys(ex).map(ch=>{const dest=ex[ch].replace("!","");return [ch,`${ROOM_NAMES[dest[0]]}${ex[ch][0]=="!"?" (one way)":""}${exitNote(dest)}`,dest]})
574}
575function legendHTML(){
576 const esc=v=>v.replace(/[&<>]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;"})[c])
577 const row=(icon,text)=>`<div class=lr><i>${icon}</i><span>${esc(text)}</span></div>`
578 const glyph=k=>k=="effigy"?"*":k=="dark"?"":k=="B"?"B":tileGlyph(k,-1,-1,1)
579 const here=legendRows().map(([k,t])=>row(GFX?gfxIcon(k):esc(glyph(k)),t))
580 const exits=exitRows().map(([ch,t,dest])=>row(GFX?gfxIcon(ch==">"?"i":dest[0]=="d"?"Dd":"D"):esc(ch),GFX&&ch!=">"?`${ch} ${t}`:t))
581 return `<div class=lg>${here.length?`here${here.join("")}`:""}exits${exits.join("")}<div class=lr><i></i><span>keys: t drop torch, . wait</span></div></div>`
582}
583
556function frame(lines,w=31){ 584function frame(lines,w=31){
557 const body=lines.map(line=>{ 585 const body=lines.map(line=>{
558 const s=(line||"").slice(0,w) 586 const s=(line||"").slice(0,w)
@@ -1032,7 +1060,7 @@ function render(){
1032 if(S.gameState=="title"){ 1060 if(S.gameState=="title"){
1033 delete G.dataset.intro 1061 delete G.dataset.intro
1034 U.innerHTML=`1MB ROGUELIKE<small>single-file campaign | minimalist descent</small>` 1062 U.innerHTML=`1MB ROGUELIKE<small>single-file campaign | minimalist descent</small>`
1035 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\ng graphics / ascii\n\nglyphs\n@ you B beast * relic\nY shrine & brazier t torch\n! crack ^ curse Q watcher\nj leech $ gold > gate` 1063 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\ng graphics / ascii\n\nglyphs\n@ you B beast * relic\nY shrine & brazier t torch\n! crack ^ curse Q watcher\nj leech $ gold > gate\n\nin play, the side panel explains\nevery item and exit in the room`
1036 L.textContent=`Pick a mode, then descend. Flame buys time; wards buy mistakes.${hasSave?" A saved run is waiting — press c to continue.":""}` 1064 L.textContent=`Pick a mode, then descend. Flame buys time; wards buy mistakes.${hasSave?" A saved run is waiting — press c to continue.":""}`
1037 X.textContent="start standard" 1065 X.textContent="start standard"
1038 X.onclick=()=>{startRun("standard",1);render()} 1066 X.onclick=()=>{startRun("standard",1);render()}
@@ -1069,7 +1097,8 @@ function render(){
1069 remember() 1097 remember()
1070 const th=floorTheme() 1098 const th=floorTheme()
1071 U.innerHTML=`floor ${S.level} | ${th.name}<small>${buildTitle()} | ${th.tag} | ${th.accent} | ${MODES[S.mode||"standard"].label} | seed ${S.seed.toString(36)}</small>` 1099 U.innerHTML=`floor ${S.level} | ${th.name}<small>${buildTitle()} | ${th.tag} | ${th.accent} | ${MODES[S.mode||"standard"].label} | seed ${S.seed.toString(36)}</small>`
1072 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`:"") 1100 I.innerHTML=legendHTML()
1101 I.insertAdjacentText("beforeend",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`:""))
1073 L.textContent=[...new Set([S.msg,S.hint,roomMsg()])].filter(Boolean).join("\n") 1102 L.textContent=[...new Set([S.msg,S.hint,roomMsg()])].filter(Boolean).join("\n")
1074 X.onclick=()=>{reset(1);render()} 1103 X.onclick=()=>{reset(1);render()}
1075 X.hidden=true 1104 X.hidden=true
1mb/sprites.js +3 −1
@@ -52,6 +52,7 @@ const SPRITES={
52 F:`<circle cx=16 cy=12 r=14 fill=url(#gGlow) /><path d="M10 28L12.5 20M22 28L19.5 20M16 29V21" stroke=url(#gIron) stroke-width=1.8 stroke-linecap=round /><path d="M6 15H26Q25 22 16 22T6 15Z" fill=url(#gIron) stroke=#0009 stroke-width=.7 /><ellipse cx=16 cy=15 rx=10 ry=2.2 fill=#1a0c05 stroke=#6a6e73 stroke-width=.7 /><ellipse cx=16 cy=15 rx=8 ry=1.4 fill=#ff7a2a opacity=.8 />`+FLAME(12.5,11,.9)+FLAME(19.5,11.5,.85)+FLAME(16,9,1.25), 52 F:`<circle cx=16 cy=12 r=14 fill=url(#gGlow) /><path d="M10 28L12.5 20M22 28L19.5 20M16 29V21" stroke=url(#gIron) stroke-width=1.8 stroke-linecap=round /><path d="M6 15H26Q25 22 16 22T6 15Z" fill=url(#gIron) stroke=#0009 stroke-width=.7 /><ellipse cx=16 cy=15 rx=10 ry=2.2 fill=#1a0c05 stroke=#6a6e73 stroke-width=.7 /><ellipse cx=16 cy=15 rx=8 ry=1.4 fill=#ff7a2a opacity=.8 />`+FLAME(12.5,11,.9)+FLAME(19.5,11.5,.85)+FLAME(16,9,1.25),
53 C:`<circle cx=16 cy=16 r=12 fill=url(#gGlow) opacity=.6 />${SHADOW}<path d="M6 24L5 10 11 15.5 16 7 21 15.5 27 10 26 24Z" fill=url(#gG) stroke=#5a360b stroke-width=.8 stroke-linejoin=round /><rect x=6 y=22 width=20 height=4 rx=1 fill=url(#gG) stroke=#5a360b stroke-width=.7 /><circle cx=5 cy=9.5 r=1.3 fill=#fff1b8 /><circle cx=16 cy=6.5 r=1.5 fill=#fff1b8 /><circle cx=27 cy=9.5 r=1.3 fill=#fff1b8 /><circle cx=11 cy=24 r=1.2 fill=#2e6bd6 /><circle cx=16 cy=24 r=1.4 fill=#c4122a /><circle cx=21 cy=24 r=1.2 fill=#1e9e5a /><path d="M8 13L8.6 20" stroke=#fff8 stroke-width=.8 />`, 53 C:`<circle cx=16 cy=16 r=12 fill=url(#gGlow) opacity=.6 />${SHADOW}<path d="M6 24L5 10 11 15.5 16 7 21 15.5 27 10 26 24Z" fill=url(#gG) stroke=#5a360b stroke-width=.8 stroke-linejoin=round /><rect x=6 y=22 width=20 height=4 rx=1 fill=url(#gG) stroke=#5a360b stroke-width=.7 /><circle cx=5 cy=9.5 r=1.3 fill=#fff1b8 /><circle cx=16 cy=6.5 r=1.5 fill=#fff1b8 /><circle cx=27 cy=9.5 r=1.3 fill=#fff1b8 /><circle cx=11 cy=24 r=1.2 fill=#2e6bd6 /><circle cx=16 cy=24 r=1.4 fill=#c4122a /><circle cx=21 cy=24 r=1.2 fill=#1e9e5a /><path d="M8 13L8.6 20" stroke=#fff8 stroke-width=.8 />`,
54 D:`<path d="M5 30V13Q5 3 16 3T27 13V30Z" fill=url(#gStone) stroke=#0009 stroke-width=.8 /><path d="M9 30V14Q9 7 16 7T23 14V30Z" fill=#050403 /><path d="M9 30V14Q9 7 16 7T23 14V30Z" fill=none style="stroke:var(--exit)" stroke-width=.8 opacity=.7 /><path d="M10 30H22" style="stroke:var(--exit)" stroke-width=2 opacity=.35 /><path d="M7 12Q8 6 13 4.5" stroke=#fff5 stroke-width=.8 fill=none />`, 54 D:`<path d="M5 30V13Q5 3 16 3T27 13V30Z" fill=url(#gStone) stroke=#0009 stroke-width=.8 /><path d="M9 30V14Q9 7 16 7T23 14V30Z" fill=#050403 /><path d="M9 30V14Q9 7 16 7T23 14V30Z" fill=none style="stroke:var(--exit)" stroke-width=.8 opacity=.7 /><path d="M10 30H22" style="stroke:var(--exit)" stroke-width=2 opacity=.35 /><path d="M7 12Q8 6 13 4.5" stroke=#fff5 stroke-width=.8 fill=none />`,
55 Dd:`<circle cx=16 cy=18 r=14 fill=url(#gHaz) opacity=.55 /><path d="M5 30V13Q5 3 16 3T27 13V30Z" fill=#15100f stroke=#000 stroke-width=.8 /><path d="M9 30V14Q9 7 16 7T23 14V30Z" fill=#000 /><path d="M9 30V14Q9 7 16 7T23 14V30Z" fill=none style="stroke:var(--hazard)" stroke-width=1 /><path d="M12.5 17.5a3.5 3.5 0 1 1 7 0c0 1.3-.7 2-1.2 2.4V21h-4.6v-1.1c-.5-.4-1.2-1.1-1.2-2.4Z" fill=#d8d0bf /><circle cx=14.6 cy=17.6 r=.9 fill=#000 /><circle cx=17.4 cy=17.6 r=.9 fill=#000 /><path d="M14.5 21v1M16 21v1M17.5 21v1" stroke=#000 stroke-width=.5 />`,
55 K:`<circle cx=16 cy=16 r=11 fill=url(#gGlow) opacity=.5 /><circle cx=10 cy=12 r=5.5 fill=none stroke=#5a360b stroke-width=3.6 /><circle cx=10 cy=12 r=5.5 fill=none stroke=url(#gG) stroke-width=2.4 /><circle cx=10 cy=12 r=1.6 fill=url(#gG) /><path d="M14 16L25.5 27.5" stroke=#5a360b stroke-width=3.4 stroke-linecap=round /><path d="M14 16L25.5 27.5" stroke=url(#gG) stroke-width=2 stroke-linecap=round /><path d="M21.5 23.5L19 26M24 26L22 28" stroke=url(#gG) stroke-width=2.2 stroke-linecap=round /><path d="M7 9.5Q8.5 8 10.5 8" stroke=#fff stroke-width=.8 fill=none opacity=.8 />` 56 K:`<circle cx=16 cy=16 r=11 fill=url(#gGlow) opacity=.5 /><circle cx=10 cy=12 r=5.5 fill=none stroke=#5a360b stroke-width=3.6 /><circle cx=10 cy=12 r=5.5 fill=none stroke=url(#gG) stroke-width=2.4 /><circle cx=10 cy=12 r=1.6 fill=url(#gG) /><path d="M14 16L25.5 27.5" stroke=#5a360b stroke-width=3.4 stroke-linecap=round /><path d="M14 16L25.5 27.5" stroke=url(#gG) stroke-width=2 stroke-linecap=round /><path d="M21.5 23.5L19 26M24 26L22 28" stroke=url(#gG) stroke-width=2.2 stroke-linecap=round /><path d="M7 9.5Q8.5 8 10.5 8" stroke=#fff stroke-width=.8 fill=none opacity=.8 />`
56} 57}
57// Grid-cell kinds from cls() that sit on top of a floor tile. 58// Grid-cell kinds from cls() that sit on top of a floor tile.
@@ -60,6 +61,7 @@ let GFX=1
60try{GFX=localStorage.getItem("mrl_gfx")!=="0"}catch(e){} 61try{GFX=localStorage.getItem("mrl_gfx")!=="0"}catch(e){}
61document.getElementById("s").insertAdjacentHTML("afterbegin",`<svg width=0 height=0 style=position:absolute aria-hidden=true><defs>${GFX_DEFS}${Object.entries(SPRITES).map(([k,v])=>`<symbol id=s-${k} viewBox="0 0 32 32">${v}</symbol>`).join("")}</defs></svg>`) 62document.getElementById("s").insertAdjacentHTML("afterbegin",`<svg width=0 height=0 style=position:absolute aria-hidden=true><defs>${GFX_DEFS}${Object.entries(SPRITES).map(([k,v])=>`<symbol id=s-${k} viewBox="0 0 32 32">${v}</symbol>`).join("")}</defs></svg>`)
62function gfxToggle(){GFX=!GFX;try{localStorage.setItem("mrl_gfx",GFX?"1":"0")}catch(e){}} 63function gfxToggle(){GFX=!GFX;try{localStorage.setItem("mrl_gfx",GFX?"1":"0")}catch(e){}}
64function gfxIcon(k){return k=="dark"?"":`<svg viewBox="0 0 32 32"><use href="#s-${k=="effigy"?"r":k=="O"?"o":k}"/></svg>`}
63function gfxCell(c,x,y,glyph){ 65function gfxCell(c,x,y,glyph){
64 const k=c.split(" ")[1] 66 const k=c.split(" ")[1]
65 if(!k||k=="u")return "" 67 if(!k||k=="u")return ""
@@ -68,6 +70,6 @@ function gfxCell(c,x,y,glyph){
68 if(k=="o")return use("o") 70 if(k=="o")return use("o")
69 const fl=use("f"+((x*7+y*13)%3)) 71 const fl=use("f"+((x*7+y*13)%3))
70 if(k=="f")return fl 72 if(k=="f")return fl
71 if(k!="i"&&R[S.r]&&k in R[S.r].ex)return fl+use("D")+`<b class=dl>${glyph}</b>` 73 if(k!="i"&&R[S.r]&&k in R[S.r].ex)return fl+use(R[S.r].ex[k].replace("!","")[0]=="d"?"Dd":"D")+`<b class=dl>${glyph}</b>`
72 return fl+(GFX_ON_FLOOR.includes(k)?use(k):`<b>${glyph}</b>`) 74 return fl+(GFX_ON_FLOOR.includes(k)?use(k):`<b>${glyph}</b>`)
73} 75}
README.org +4 −2
@@ -22,7 +22,8 @@ controls:
22- c continue a saved run (title screen) 22- c continue a saved run (title screen)
23#+end_example 23#+end_example
24 24
25on mobile an on-screen d-pad appears. 25on mobile an on-screen d-pad appears. the side panel lists what
26every item and exit in the current room does.
26 27
27#+begin_example 28#+begin_example
28modes (press a number on the title screen): 29modes (press a number on the title screen):
@@ -35,7 +36,8 @@ win once and a hidden seventh floor opens.
35budget: the game is the raw files in 1mb/, no build step, and 36budget: the game is the raw files in 1mb/, no build step, and
36together they stay under 1,048,576 bytes (1 MiB). tools/check.sh is 37together they stay under 1,048,576 bytes (1 MiB). tools/check.sh is
37the gate; it prints the byte ledger and proves every escape room 38the gate; it prints the byte ledger and proves every escape room
38stays winnable across hundreds of seeds. ledger history in 39stays winnable across hundreds of seeds, and that every item
40that can appear has a side-panel explanation. ledger history in
39docs/SIZES.txt. 41docs/SIZES.txt.
40 42
41#+begin_src sh 43#+begin_src sh
docs/SIZES.txt +5 −4
@@ -3,7 +3,8 @@
3Raw bytes of everything in `1mb/`, against the 1,048,576-byte (1 MiB) budget. 3Raw bytes of everything in `1mb/`, against the 1,048,576-byte (1 MiB) budget.
4Record a row after each phase of work; `tools/check-size.sh` prints the current numbers. 4Record a row after each phase of work; `tools/check-size.sh` prints the current numbers.
5 5
6| Phase | Date | index.html | data.js | Total | % of budget | 6| Phase | Date | index.html | data.js | sprites.js | Total | % of budget |
7|---|---|---:|---:|---:|---:| 7|---|---|---:|---:|---:|---:|---:|
8| Phase 0 — guardrails (no game changes) | 2026-07-12 | 55,225 | 27,015 | 82,240 | 7.8% | 8| Phase 0 — guardrails (no game changes) | 2026-07-12 | 55,225 | 27,015 | — | 82,240 | 7.8% |
9| Phase 1 — engine enablers (data-driven graph, procgen rooms, full-floor solver) | 2026-07-12 | 61,356 | 28,998 | 90,354 | 8.6% | 9| Phase 1 — engine enablers (data-driven graph, procgen rooms, full-floor solver) | 2026-07-12 | 61,356 | 28,998 | — | 90,354 | 8.6% |
10| Graphics — vector tiles, room legend, dark-step cue | 2026-09-27 | 64,192 | 30,340 | 17,943 | 112,475 | 10.7% |
tools/validate.js +29
@@ -39,6 +39,7 @@ function el() {
39 onclick: null, 39 onclick: null,
40 classList: { toggle() {} }, 40 classList: { toggle() {} },
41 insertAdjacentHTML() {}, 41 insertAdjacentHTML() {},
42 insertAdjacentText() {},
42 } 43 }
43} 44}
44 45
@@ -107,3 +108,31 @@ if (fails.length) {
107} 108}
108console.log(`validate: OK — ${checks} full floors solvable (${seedCount} seeds x floors 1-7, ${elapsed}s)`) 109console.log(`validate: OK — ${checks} full floors solvable (${seedCount} seeds x floors 1-7, ${elapsed}s)`)
109 110
111// Every interactable that can appear on floors 1-7 needs a LEGEND line and
112// every exit destination a ROOM_NAMES entry. Room d kills on entry, so its
113// contents are never shown.
114const missing = JSON.parse(vm.runInContext(`JSON.stringify((() => {
115 const miss = {}
116 for (const s0 of ${seedsJson}) {
117 for (const level of [1, 2, 3, 4, 5, 6, 7]) {
118 S = { level, mods: pickMods(s0), flags: {}, drop: null, seen: {} }
119 build(s0)
120 for (const r in R) {
121 if (r[0] == "d") continue
122 for (const ch in R[r].ex) if (!ROOM_NAMES[R[r].ex[ch].replace("!", "")[0]]) miss["exit " + r + ":" + ch] = level
123 for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
124 const t = tile(x, y, r)
125 if (t != "." && t != "#" && t != ">" && !(t in R[r].ex) && !LEGEND[t]) miss["tile " + t] = level
126 }
127 if (O.effigies.some(o => o.r == r) && !LEGEND.effigy) miss.effigy = level
128 }
129 }
130 }
131 return miss
132})())`, ctx, { filename: "validate-legend" }))
133const gaps = Object.keys(missing)
134if (gaps.length) {
135 console.error(`validate: ${gaps.length} interactables without a legend entry: ${gaps.map(g => `${g} (floor ${missing[g]})`).join(", ")}`)
136 process.exit(1)
137}
138console.log("validate: OK — every interactable has a legend entry")