Convert Twitch controls to video overlay

This commit is contained in:
2026-08-17 13:49:39 -07:00
parent 9ebd1ea283
commit a8a86852c0
8 changed files with 182 additions and 16 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
let state=null,session=null,userId=null,lastSequence=0,activeSocket=null,extensionIdentityPending=false
const extensionMode=location.pathname==='/extension';document.body.classList.toggle('extension-mode',extensionMode)
const extensionMode=location.pathname==='/extension',overlayParams=new window.URLSearchParams(location.search),localOverlayHost=['localhost','127.0.0.1','[::1]'].includes(location.hostname),overlayDevMode=extensionMode&&localOverlayHost&&overlayParams.get('dev')==='1';document.documentElement.classList.toggle('extension-mode',extensionMode);document.documentElement.classList.toggle('overlay-debug',extensionMode&&localOverlayHost&&overlayParams.get('debug')==='1');document.documentElement.classList.toggle('overlay-dev',overlayDevMode)
const $=s=>document.querySelector(s), $$=s=>document.querySelectorAll(s)
const escapeHtml=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))
function disabledReason(){if(extensionIdentityPending)return 'Share your Twitch identity to enable controls.';if(!userId)return extensionMode?'Waiting for Twitch authorization.':'Broadcast mode — log in locally to test controls.';const p=state?.players.find(x=>x.twitchUserId===userId);if(!p)return extensionMode?'Click Spawn character to join.':'Type !spawn in chat first.';if(p.lifeState==='dead')return 'Your character is dead.';if(state.phase.kind!=='player')return 'Wait for Player Phase.';if(!p.ap)return 'No AP remains this phase.';return ''}
@@ -9,7 +9,7 @@ function render(){if(!state)return;$('#floor').textContent=`Floor ${state.floorN
const map=$('#map');map.style.gridTemplateColumns=`repeat(${state.floor.width},auto)`;map.innerHTML=''
for(let y=0;y<state.floor.height;y++)for(let x=0;x<state.floor.width;x++){const el=document.createElement('div'),tile=state.floor.tiles[y][x];el.className=`tile ${tile}`;if(tile==='exit')el.textContent='▣';const p=state.players.find(p=>p.lifeState==='alive'&&p.position.x===x&&p.position.y===y);const gob=state.goblin.mode!=='dead'&&state.goblin.position.x===x&&state.goblin.position.y===y;if(gob||p){const e=document.createElement('span');e.className=`entity ${gob?'goblin':'player'}`;e.textContent=gob?'◆':'●';e.title=gob?'Goblin':p.displayName;el.append(e)}map.append(el)}
$('#log').innerHTML=state.actionLog.slice(-40).map(e=>`<li><small>#${e.sequence}</small> ${escapeHtml(e.message)}</li>`).join('');$('#log').scrollTop=$('#log').scrollHeight
const reason=disabledReason();$('#disabled').textContent=reason;$$('[data-command]').forEach(b=>b.disabled=Boolean(reason));const spawnButton=$('#spawnExtension');spawnButton.hidden=!extensionMode;spawnButton.disabled=!session||Boolean(me);spawnButton.textContent=me?'Character spawned':'Spawn character'
const reason=disabledReason();$('#disabled').textContent=reason;$$('[data-command]').forEach(b=>b.disabled=Boolean(reason));const spawnButton=$('#spawnExtension');spawnButton.hidden=!extensionMode||overlayDevMode;spawnButton.disabled=!session||Boolean(me);spawnButton.textContent=me?'Character spawned':'Spawn character'
}
async function api(path,options={}){const res=await fetch(path,{...options,headers:{'content-type':'application/json',...(session?{authorization:`Bearer ${session}`}:{})}});const data=await res.json();if(!res.ok)throw new Error(data.message||data.error||data.reason||'Request failed');return data}
async function authorizeExtension(token){const auth=await api('/api/extension/session',{method:'POST',body:JSON.stringify({token})});session=auth.token;userId=auth.twitchUserId;state=auth.state;if(activeSocket?.readyState===WebSocket.OPEN)activeSocket.send(JSON.stringify({type:'authenticate',token:session}));render()}