(function(){ const qs = new URLSearchParams(window.location.search); const token = qs.get('token'); const panel = document.getElementById('panel'); const list = document.getElementById('list'); const roundEl = document.getElementById('round'); const updatedEl = document.getElementById('updated'); const countEl = document.getElementById('count'); let ICONS = {}; let EMOJI = {}; let KNOWN = []; function fmtAgo(ms){ const d=Date.now()-ms; if(d<1500)return 'just now'; const s=Math.floor(d/1000); if(s<60) return s+'s ago'; const m=Math.floor(s/60); if(m<60) return m+'m ago'; const h=Math.floor(m/60); return h+'h ago'; } function avatarSrc(a){ if(!a.avatar) return ''; if(a.avatar.startsWith('http') || a.avatar.startsWith('/')) return a.avatar; return '/avatars/' + encodeURIComponent(a.avatar); } function iconSrc(key){ const f = ICONS[key]; return f ? ('/icons/' + encodeURIComponent(f)) : '' } function render(state){ ICONS = state.condition_icons||{}; EMOJI = state.condition_emoji||{}; KNOWN = state.known_conditions||[]; const deadMode = state.dead_mode || 'normal'; if(!state.visible){ panel.classList.add('hideall'); return; } else { panel.classList.remove('hideall'); } roundEl.textContent = state.round; updatedEl.textContent = fmtAgo(state.updated_at); list.innerHTML = ''; (state.actors||[]).forEach((a, idx)=>{ if(!a.visible) return; if(a.dead && deadMode==='hide') return; const cls = ['row','type-'+(a.type||'pc')]; if(idx===state.turn_idx) cls.push('active'); if(a.dead){ cls.push('dead'); if(deadMode==='shrink') cls.push('shrunk'); } const card = document.createElement('div'); card.className = cls.join(' '); // Portrait const portrait = document.createElement(a.avatar? 'img':'div'); if(a.avatar){ portrait.src = avatarSrc(a); portrait.className='portrait'; portrait.alt=a.name; } else{ portrait.className='noavatar'; } card.appendChild(portrait); // Death & Concentration badges if(a.dead){ const db = document.createElement('div'); db.className='deathBadge'; db.textContent='☠'; card.appendChild(db); } if(a.conc){ const cs = document.createElement('div'); cs.className='concStar'; cs.textContent='✦'; card.appendChild(cs); } // Name + meta const name = document.createElement('div'); name.className='name'; const n = document.createElement('div'); n.className='n'; n.textContent=a.name; name.appendChild(n); const meta = document.createElement('div'); meta.className='meta'; const bits = []; if(a.hp){ bits.push('HP '+a.hp); } if(a.reveal_ac && a.ac){ bits.push('AC '+a.ac); } bits.push('Init '+Math.floor(a.init)); if(a.note){ bits.push(a.note); } meta.textContent = bits.join(' · '); name.appendChild(meta); card.appendChild(name); // Conditions row if (Array.isArray(a.effects) && a.effects.length){ const tags = document.createElement('div'); tags.className='tags'; a.effects.forEach(e=>{ const t=document.createElement('span'); t.className='tag'; const wrap=document.createElement('span'); wrap.className='ico_wrap'; const src=iconSrc(e); if(src){ const i=document.createElement('img'); i.className='ico'; i.alt=e; i.src=src; wrap.appendChild(i); } else { wrap.textContent='•'; } t.appendChild(wrap); const lbl=document.createElement('span'); lbl.textContent=e; t.appendChild(lbl); tags.appendChild(t); }); card.appendChild(tags); } list.appendChild(card); }); countEl.textContent = list.childElementCount + ' shown'; } let last = null; let since = 0; async function poll(){ try{ const r = await fetch(`/api/state?since=${since}&token=${encodeURIComponent(token)}`); if(!r.ok){ throw new Error('state '+r.status); } const s = await r.json(); last = s; render(s); since = s.version; }catch(e){ } setTimeout(poll, 200); } setInterval(()=>{ if(last) updatedEl.textContent = fmtAgo(last.updated_at); }, 3000); poll(); })();