Commit 0c7885650f
Verified · cmc
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. | ||
| 1026 | const 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 | } | ||
| 1046 | const 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"} | ||
| 1025 | const RELICS={ | 1047 | const 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 | ||
| 556 | function 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 | } | ||
| 562 | function 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 | } | ||
| 571 | function 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 | } | ||
| 575 | function legendHTML(){ | ||
| 576 | const esc=v=>v.replace(/[&<>]/g,c=>({"&":"&","<":"<",">":">"})[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 | |||
| 556 | function frame(lines,w=31){ | 584 | function 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 | |||
| 60 | try{GFX=localStorage.getItem("mrl_gfx")!=="0"}catch(e){} | 61 | try{GFX=localStorage.getItem("mrl_gfx")!=="0"}catch(e){} |
| 61 | document.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>`) | 62 | document.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>`) |
| 62 | function gfxToggle(){GFX=!GFX;try{localStorage.setItem("mrl_gfx",GFX?"1":"0")}catch(e){}} | 63 | function gfxToggle(){GFX=!GFX;try{localStorage.setItem("mrl_gfx",GFX?"1":"0")}catch(e){}} |
| 64 | function gfxIcon(k){return k=="dark"?"":`<svg viewBox="0 0 32 32"><use href="#s-${k=="effigy"?"r":k=="O"?"o":k}"/></svg>`} | ||
| 63 | function gfxCell(c,x,y,glyph){ | 65 | function 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 | ||
| 25 | on mobile an on-screen d-pad appears. | 25 | on mobile an on-screen d-pad appears. the side panel lists what |
| 26 | every item and exit in the current room does. | ||
| 26 | 27 | ||
| 27 | #+begin_example | 28 | #+begin_example |
| 28 | modes (press a number on the title screen): | 29 | modes (press a number on the title screen): |
| @@ -35,7 +36,8 @@ win once and a hidden seventh floor opens. | |||
| 35 | budget: the game is the raw files in 1mb/, no build step, and | 36 | budget: the game is the raw files in 1mb/, no build step, and |
| 36 | together they stay under 1,048,576 bytes (1 MiB). tools/check.sh is | 37 | together they stay under 1,048,576 bytes (1 MiB). tools/check.sh is |
| 37 | the gate; it prints the byte ledger and proves every escape room | 38 | the gate; it prints the byte ledger and proves every escape room |
| 38 | stays winnable across hundreds of seeds. ledger history in | 39 | stays winnable across hundreds of seeds, and that every item |
| 40 | that can appear has a side-panel explanation. ledger history in | ||
| 39 | docs/SIZES.txt. | 41 | docs/SIZES.txt. |
| 40 | 42 | ||
| 41 | #+begin_src sh | 43 | #+begin_src sh |
docs/SIZES.txt +5 −4
| @@ -3,7 +3,8 @@ | |||
| 3 | Raw bytes of everything in `1mb/`, against the 1,048,576-byte (1 MiB) budget. | 3 | Raw bytes of everything in `1mb/`, against the 1,048,576-byte (1 MiB) budget. |
| 4 | Record a row after each phase of work; `tools/check-size.sh` prints the current numbers. | 4 | Record 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 | } |
| 108 | console.log(`validate: OK — ${checks} full floors solvable (${seedCount} seeds x floors 1-7, ${elapsed}s)`) | 109 | console.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. | ||
| 114 | const 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" })) | ||
| 133 | const gaps = Object.keys(missing) | ||
| 134 | if (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 | } | ||
| 138 | console.log("validate: OK — every interactable has a legend entry") | ||