โ back to AbramsEgo
feat(dungeon): sound FX (WebAudio synth โ combat clash, delivery chime, alert boom, slain fanfare; ๐ toggle, off by default) + clickable minimap w/ live viewport rect + sessions 'type' (blinking cursor + green code particles float up)
08e18a23f25981086841aefc0e41e52d3089c4d1 ยท 2026-07-02 08:02:45 -0700 ยท Steve
Files touched
M data/local-fleet.jsonM public/dungeon.html
Diff
commit 08e18a23f25981086841aefc0e41e52d3089c4d1
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jul 2 08:02:45 2026 -0700
feat(dungeon): sound FX (WebAudio synth โ combat clash, delivery chime, alert boom, slain fanfare; ๐ toggle, off by default) + clickable minimap w/ live viewport rect + sessions 'type' (blinking cursor + green code particles float up)
---
data/local-fleet.json | 2 +-
public/dungeon.html | 49 ++++++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 45 insertions(+), 6 deletions(-)
diff --git a/data/local-fleet.json b/data/local-fleet.json
index 4117abd..4b37e24 100644
--- a/data/local-fleet.json
+++ b/data/local-fleet.json
@@ -1 +1 @@
-{"fetchedAt":"2026-07-02T14:47:12.101Z","procs":[{"name":"pm2-logrotate","status":"online","cpu":0.3,"memMB":28,"restarts":0,"uptimeMs":1903004,"startedAt":"2026-07-02T14:15:29.097Z"},{"name":"abramsego","status":"online","cpu":0.4,"memMB":64,"restarts":9,"uptimeMs":241441,"startedAt":"2026-07-02T14:43:10.660Z"},{"name":"api-token-dashboard","status":"online","cpu":0.2,"memMB":26,"restarts":0,"uptimeMs":1781390,"startedAt":"2026-07-02T14:17:30.711Z"},{"name":"pattern-vault","status":"online","cpu":0.2,"memMB":29,"restarts":1,"uptimeMs":1270620,"startedAt":"2026-07-02T14:26:01.481Z"},{"name":"agent-cabinet","status":"online","cpu":0.2,"memMB":28,"restarts":0,"uptimeMs":1778666,"startedAt":"2026-07-02T14:17:33.435Z"},{"name":"astek-landing","status":"online","cpu":0.1,"memMB":51,"restarts":0,"uptimeMs":275904,"startedAt":"2026-07-02T14:42:36.197Z"}]}
\ No newline at end of file
+{"fetchedAt":"2026-07-02T15:02:36.091Z","procs":[{"name":"pm2-logrotate","status":"online","cpu":0.2,"memMB":27,"restarts":0,"uptimeMs":2826994,"startedAt":"2026-07-02T14:15:29.097Z"},{"name":"abramsego","status":"online","cpu":0.7,"memMB":49,"restarts":10,"uptimeMs":901055,"startedAt":"2026-07-02T14:47:35.036Z"},{"name":"api-token-dashboard","status":"online","cpu":0.2,"memMB":25,"restarts":0,"uptimeMs":2705380,"startedAt":"2026-07-02T14:17:30.711Z"},{"name":"pattern-vault","status":"online","cpu":0.1,"memMB":26,"restarts":1,"uptimeMs":2194610,"startedAt":"2026-07-02T14:26:01.481Z"},{"name":"agent-cabinet","status":"online","cpu":0.1,"memMB":25,"restarts":0,"uptimeMs":2702656,"startedAt":"2026-07-02T14:17:33.435Z"},{"name":"astek-landing","status":"online","cpu":0.1,"memMB":38,"restarts":0,"uptimeMs":1199894,"startedAt":"2026-07-02T14:42:36.197Z"}]}
\ No newline at end of file
diff --git a/public/dungeon.html b/public/dungeon.html
index 37f4556..f046c7d 100644
--- a/public/dungeon.html
+++ b/public/dungeon.html
@@ -37,6 +37,7 @@ canvas{display:block;cursor:grab}canvas:active{cursor:grabbing}
.hint{position:fixed;bottom:12px;left:18px;z-index:10;color:var(--mut);font-size:11.5px}
#zoombar{position:fixed;bottom:12px;right:18px;z-index:10;display:flex;gap:6px}
#zoombar button{width:30px;height:30px;border-radius:7px;border:1px solid #2a2338;background:#140f20;color:var(--ink);cursor:pointer;font-size:16px}
+#mini{position:fixed;left:18px;bottom:44px;z-index:10;width:200px;height:132px;border:1px solid #2a2338;border-radius:8px;background:#0a0810cc;cursor:pointer}
</style></head><body>
<div id="hud">
<h1>The <em>Dungeon</em></h1>
@@ -52,7 +53,8 @@ canvas{display:block;cursor:grab}canvas:active{cursor:grabbing}
<div id="tip"></div>
<div id="panel"><div class="close" onclick="closePanel()">โ</div><div id="panelBody"></div></div>
<div class="hint">drag to pan ยท scroll to zoom ยท click an agent to see what it's doing ยท auto-refreshes every 15s</div>
-<div id="zoombar"><button onclick="zoomBy(1.2)">+</button><button onclick="zoomBy(0.83)">โ</button><button onclick="resetView()">โ</button></div>
+<canvas id="mini"></canvas>
+<div id="zoombar"><button id="sfxBtn" onclick="toggleSfx()" title="sound">๐</button><button onclick="zoomBy(1.2)">+</button><button onclick="zoomBy(0.83)">โ</button><button onclick="resetView()">โ</button></div>
<script>
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
let W,H,DPR;function resize(){DPR=devicePixelRatio||1;W=cv.width=innerWidth*DPR;H=cv.height=innerHeight*DPR;cv.style.width=innerWidth+'px';cv.style.height=innerHeight+'px';}
@@ -71,6 +73,37 @@ function updateParticles(){for(const p of particles){p.x+=p.vx;p.y+=p.vy;p.vy+=0
function drawParticles(){for(const p of particles){ctx.globalAlpha=Math.max(0,p.life);ctx.fillStyle=p.color;
ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,6.28);ctx.fill();}ctx.globalAlpha=1;}
+// ---- sound (WebAudio synth, off by default) ----
+let sfxOn=false,actx=null,lastKey=0;
+function toggleSfx(){sfxOn=!sfxOn;document.getElementById('sfxBtn').textContent=sfxOn?'๐':'๐';
+ if(sfxOn&&!actx){actx=new(window.AudioContext||window.webkitAudioContext)();}}
+function beep(freq,dur,type,vol){if(!sfxOn||!actx)return;const o=actx.createOscillator(),g=actx.createGain();
+ o.type=type||'square';o.frequency.value=freq;g.gain.value=(vol||.05);o.connect(g).connect(actx.destination);
+ const n=actx.currentTime;g.gain.setValueAtTime(vol||.05,n);g.gain.exponentialRampToValueAtTime(.0001,n+(dur||.1));
+ o.start(n);o.stop(n+(dur||.1));}
+const sfx={clash:()=>beep(320+Math.random()*140,.06,'square',.03),
+ deliver:()=>{beep(660,.07,'sine',.04);setTimeout(()=>beep(990,.08,'sine',.04),60);},
+ boom:()=>beep(70,.35,'sawtooth',.08),
+ slain:()=>{beep(523,.09,'triangle',.05);setTimeout(()=>beep(784,.12,'triangle',.05),90);},
+ type:()=>{const now=performance.now();if(now-lastKey>55){lastKey=now;beep(1400+Math.random()*400,.02,'square',.015);}}};
+
+// ---- minimap ----
+const mini=document.getElementById('mini'),mctx=mini.getContext('2d');
+function drawMini(){const w=mini.width=200,h=mini.height=132;mctx.clearRect(0,0,w,h);
+ if(!chambers.length)return;const b=worldBounds();const sw=b.x1-b.x0||1,sh=b.y1-b.y0||1;
+ const pad=8,sc=Math.min((w-2*pad)/sw,(h-2*pad)/sh),ox=pad-b.x0*sc,oy=pad-b.y0*sc;
+ chambers.forEach(c=>{mctx.fillStyle='#221a33';mctx.fillRect(c.x*sc+ox,c.y*sc+oy,c.w*sc,c.h*sc);});
+ agents.forEach(a=>{const st=statusOf(a);mctx.fillStyle=COL[st];
+ mctx.fillRect(a.x*sc+ox-1,a.y*sc+oy-1,st==='bad'?3:2,st==='bad'?3:2);});
+ monsters.forEach(m=>{mctx.fillStyle='#e5484d';mctx.fillRect(m.x*sc+ox-1,m.y*sc+oy-1,3,3);});
+ // viewport rect
+ const vx=cam.x*sc+ox,vy=cam.y*sc+oy,vw=(innerWidth/cam.z)*sc,vh=(innerHeight/cam.z)*sc;
+ mctx.strokeStyle='#d4af6a';mctx.lineWidth=1;mctx.strokeRect(vx,vy,vw,vh);
+ mini._m={sc,ox,oy};}
+mini.addEventListener('click',e=>{if(!mini._m)return;const r=mini.getBoundingClientRect();
+ const {sc,ox,oy}=mini._m;const wx=((e.clientX-r.left)-ox)/sc,wy=((e.clientY-r.top)-oy)/sc;
+ cam.x=wx-innerWidth/2/cam.z;cam.y=wy-innerHeight/2/cam.z;});
+
// status โ color/kind
function statusOf(a){
if(a.kind==='canary') return a.bad?'bad':'online';
@@ -160,7 +193,7 @@ function build(d){
const keyOf=a=>a.kind+':'+a.name;
const curKeys=new Set(alerts.map(keyOf));
// new alerts โ explosion at that agent's chamber
- alerts.forEach(a=>{if(!prevAlertKeys.has(keyOf(a))){burst(a.x,a.y,'#e5484d',26,3.2);burst(a.x,a.y,'#ffb020',14,2.2);}});
+ alerts.forEach(a=>{if(!prevAlertKeys.has(keyOf(a))){burst(a.x,a.y,'#e5484d',26,3.2);burst(a.x,a.y,'#ffb020',14,2.2);sfx.boom();}});
// build monster list (one per active alert), engaged by the nearest sentry
const sentries=agents.filter(a=>a.kind==='canary'&&!a.bad);
monsters=alerts.map(a=>{
@@ -169,7 +202,7 @@ function build(d){
return prev? Object.assign(prev,{target:a,foe}):
{key:keyOf(a),target:a,foe,x:a.x+30,y:a.y-30,ph:Math.random()*6.28,hp:1};});
// cleared alerts โ poof + tally
- prevAlertKeys.forEach(k=>{if(!curKeys.has(k)){slain++;}});
+ prevAlertKeys.forEach(k=>{if(!curKeys.has(k)){slain++;sfx.slain();}});
prevAlertKeys=curKeys;
S('sSlain',slain);
// hud
@@ -261,7 +294,7 @@ function draw(){
a.x+=dx/dist*3.2; a.y+=dy/dist*3.2;
if((t>>1)%2===0)particles.push({x:a.x,y:a.y,vx:0,vy:0,life:.5,color:'#5b8cff',r:2}); // trail
if(dist<26){
- if(a.travel.phase==='go'){a.travel.phase='return';burst(tx,ty,'#5b8cff',10,1.6);} // deliver
+ if(a.travel.phase==='go'){a.travel.phase='return';burst(tx,ty,'#5b8cff',10,1.6);sfx.deliver();} // deliver
else{a.chamber=a.travel.home;a.x=a.travel.hx;a.y=a.travel.hy;a.travel=null;}}
} else {
// wander within chamber
@@ -286,6 +319,11 @@ function draw(){
if(st==='work'&&(t>>4)%2===0){ctx.font='11px sans-serif';ctx.fillStyle='#5b8cff';
ctx.fillText('โขโขโข',a.x+a.r,a.y-a.r);}
if(st==='bad'){ctx.font='12px sans-serif';ctx.fillText('!',a.x+a.r-2,a.y-a.r);}
+ // live sessions "type": blinking cursor + code particles floating up
+ if(a.kind==='session'){
+ if((t>>4)%2===0){ctx.fillStyle='#37d67a';ctx.font='bold 12px monospace';ctx.fillText('โ',a.x+a.r-1,a.y-a.r+2);}
+ if(t%12===0){particles.push({x:a.x+(Math.random()-.5)*14,y:a.y-a.r,vx:(Math.random()-.5)*.3,vy:-.8,life:1,color:'#37d67a',r:1.6});sfx.type();}
+ }
// label when zoomed in enough
if(cam.z>0.5){ctx.fillStyle='#8a7fa8';ctx.font='10px Avenir Next,sans-serif';ctx.textAlign='center';
ctx.fillText(a.name.slice(0,16),a.x,a.y+a.r+11);ctx.textAlign='left';}
@@ -298,7 +336,7 @@ function draw(){
if(m.foe){const f=m.foe;const dx=m.x-f.x,dy=by-f.y,d=Math.hypot(dx,dy);
if(d>60){f.x+=dx/d*1.6;f.y+=dy/d*1.6;} // charge in
else if(t%18===0){ // clash
- const mx=(f.x+m.x)/2,my=(f.y+by)/2;burst(mx,my,'#ffd24a',6,2);burst(mx,my,'#e5484d',4,1.6);}
+ const mx=(f.x+m.x)/2,my=(f.y+by)/2;burst(mx,my,'#ffd24a',6,2);burst(mx,my,'#e5484d',4,1.6);sfx.clash();}
// sword line
ctx.strokeStyle='#ffd24a55';ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(f.x,f.y);ctx.lineTo(m.x,by);ctx.stroke();}
// menace aura + body
@@ -310,6 +348,7 @@ function draw(){
ctx.fillText((tg.verdict||tg.status||'ALERT').toString().toUpperCase(),m.x,by-24);ctx.textAlign='left';
});
drawParticles();updateParticles();
+ if(t%3===0)drawMini();
requestAnimationFrame(draw);
}
function roundRect(x,y,w,h,r){ctx.beginPath();ctx.moveTo(x+r,y);ctx.arcTo(x+w,y,x+w,y+h,r);
โ b64165a feat(dungeon): inter-room handoff journeys (workers carry wo
ยท
back to AbramsEgo
ยท
feat(dungeon): chamber health-on-hover (agents/working/cpu/a 99d82e9 โ