Files
encounterflow/encounterflow/static/js/board.js

74 lines
4.0 KiB
JavaScript

(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();
})();