// ── STATE ────────────────────────────────────────── let level = 1; let fpCount = 0; let primaryClasses = []; let otherClasses = []; let spells = []; let bonds = [ { name: '', feelings: [] }, { name: '', feelings: [] }, { name: '', feelings: [] }, { name: '', feelings: [] }, { name: '', feelings: [] }, { name: '', feelings: [] }, ]; const STATUSES = ['Slow', 'Enraged', 'Dazed', 'Weak', 'Poisoned', 'Shaken']; const FEELINGS = ['Admiration', 'Inferiority', 'Loyalty', 'Mistrust', 'Affection', 'Hatred']; // ── TABS ─────────────────────────────────────────── function switchTab(tab) { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); event.currentTarget.classList.add('active'); document.getElementById('page-' + tab).classList.add('active'); } // ── INIT ─────────────────────────────────────────── function init() { const savedTheme = localStorage.getItem('fabulaUltimaTheme') || (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'); document.getElementById('themeToggle').textContent = savedTheme === 'light' ? '☾ Dark' : '☀ Light'; renderStatuses(); renderFP(); renderBonds(); renderPrimaryClasses(); renderOtherClasses(); renderSpells(); updateXPBar(); tryAutoLoad(); } // ── STATUS EFFECTS ───────────────────────────────── function renderStatuses() { const g = document.getElementById('statusGrid'); g.innerHTML = ''; STATUSES.forEach(s => { const el = document.createElement('div'); el.className = 'status-item'; el.dataset.status = s; el.onclick = () => { el.classList.toggle('active-status'); el.querySelector('.status-check').textContent = el.classList.contains('active-status') ? '✗' : ''; }; el.innerHTML = `
${s}
`; g.appendChild(el); }); } // ── FABULA POINTS ────────────────────────────────── function renderFP() { fpCount = parseInt(document.getElementById('fpCount').value) || 0; const pips = document.getElementById('fpPips'); const total = Math.max(10, fpCount); pips.innerHTML = ''; for (let i = 0; i < total; i++) { const pip = document.createElement('div'); pip.className = 'fp-pip' + (i < fpCount ? ' filled' : ''); pip.onclick = () => { const newVal = i < fpCount ? i : i + 1; document.getElementById('fpCount').value = newVal; renderFP(); }; pips.appendChild(pip); } } // ── BONDS ────────────────────────────────────────── function renderBonds() { const container = document.getElementById('bondsContainer'); container.innerHTML = ''; bonds.forEach((bond, idx) => { const el = document.createElement('div'); el.className = 'bond-block'; el.innerHTML = `
${idx + 1}
${FEELINGS.map(f => `
${bond.feelings.includes(f) ? '✓' : ''}
${f}
`).join('')}
`; container.appendChild(el); }); } function toggleFeeling(bondIdx, feeling, el) { const bond = bonds[bondIdx]; if (bond.feelings.includes(feeling)) { bond.feelings = bond.feelings.filter(f => f !== feeling); el.classList.remove('active'); el.querySelector('.bond-feeling-box').textContent = ''; } else { bond.feelings.push(feeling); el.classList.add('active'); el.querySelector('.bond-feeling-box').textContent = '✓'; } } // ── LEVEL ────────────────────────────────────────── function adjustLevel(delta) { level = Math.max(1, Math.min(50, level + delta)); document.getElementById('levelDisplay').textContent = level; } // ── XP BAR ───────────────────────────────────────── function updateXPBar() { const xp = parseInt(document.getElementById('xpCurrent').value) || 0; const pct = Math.min((xp % 10) * 10, 100); document.getElementById('xpBar').style.width = pct + '%'; document.getElementById('xpVal').textContent = xp + ' XP'; } // ── STAT CALC ────────────────────────────────────── function recalcStats() { /* auto-calc on keyup */ } function calcHP() { const mig = parseInt(document.getElementById('mig-base').value) || 6; const max = mig * 5 + level; document.getElementById('hpMax').value = max; if (!document.getElementById('hpCur').value) document.getElementById('hpCur').value = max; updateCrisis(); } function calcMP() { const wlp = parseInt(document.getElementById('wlp-base').value) || 6; const max = wlp * 5 + level; document.getElementById('mpMax').value = max; if (!document.getElementById('mpCur').value) document.getElementById('mpCur').value = max; } function updateCrisis() { const max = parseInt(document.getElementById('hpMax').value) || 0; const cur = parseInt(document.getElementById('hpCur').value) || 0; const badge = document.getElementById('crisisBadge'); badge.style.display = (max > 0 && cur <= Math.floor(max / 2)) ? 'block' : 'none'; } // ── MARTIAL ──────────────────────────────────────── function toggleMartial(el) { el.classList.toggle('checked'); el.querySelector('.martial-box').textContent = el.classList.contains('checked') ? '✓' : ''; } // ── DISCIPLINES ──────────────────────────────────── function toggleDisc(el) { el.classList.toggle('checked'); el.querySelector('.disc-box').textContent = el.classList.contains('checked') ? '✓' : ''; } // ── CLASSES ──────────────────────────────────────── function classBlockHTML(cls, idx, type) { return `
`; } function renderPrimaryClasses() { document.getElementById('primaryClassContainer').innerHTML = primaryClasses.map((c, i) => classBlockHTML(c, i, 'primary')).join(''); } function renderOtherClasses() { document.getElementById('otherClassContainer').innerHTML = otherClasses.map((c, i) => classBlockHTML(c, i, 'other')).join(''); } function addPrimaryClass() { primaryClasses.push({}); renderPrimaryClasses(); } function addOtherClass() { otherClasses.push({}); renderOtherClasses(); } function removeClass(type, idx) { if (type === 'primary') { primaryClasses.splice(idx, 1); renderPrimaryClasses(); } else { otherClasses.splice(idx, 1); renderOtherClasses(); } } // ── SPELLS ───────────────────────────────────────── function renderSpells() { const tbody = document.getElementById('spellsBody'); tbody.innerHTML = spells.map((s, i) => ` `).join(''); } function addSpell() { spells.push({}); renderSpells(); } function removeSpell(i) { spells.splice(i, 1); renderSpells(); } // ── SAVE / LOAD ──────────────────────────────────── function collectData() { const get = id => { const el = document.getElementById(id); return el ? el.value : ''; }; const statusesActive = [...document.querySelectorAll('.status-item.active-status')].map(el => el.dataset.status); const martialChecked = [...document.querySelectorAll('.martial-item.checked')].map(el => el.querySelector('span').textContent); const disciplinesChecked = [...document.querySelectorAll('.disc-item.checked')].map(el => el.querySelector('span').textContent); return { name: get('charName'), pronouns: get('charPronouns'), identity: get('charIdentity'), theme: get('charTheme'), origin: get('charOrigin'), traits: get('charTraits'), level, xp: get('xpCurrent'), zenit: get('zenit'), initMod: get('initMod'), defense: get('defense'), magDef: get('magDef'), dexBase: get('dex-base'), dexCur: get('dex-cur'), insBase: get('ins-base'), insCur: get('ins-cur'), migBase: get('mig-base'), migCur: get('mig-cur'), wlpBase: get('wlp-base'), wlpCur: get('wlp-cur'), hpMax: get('hpMax'), hpCur: get('hpCur'), mpMax: get('mpMax'), mpCur: get('mpCur'), ipMax: get('ipMax'), ipCur: get('ipCur'), fp: get('fpCount'), backpack: get('backpack'), accName: get('acc-name'), accDesc: get('acc-desc'), armName: get('arm-name'), armDesc: get('arm-desc'), mhName: get('mh-name'), mhDesc: get('mh-desc'), ohName: get('oh-name'), ohDesc: get('oh-desc'), heroicSkills: get('heroicSkills'), ritualsNotes: get('ritualsNotes'), statusesActive, martialChecked, disciplinesChecked, bonds, primaryClasses, otherClasses, spells }; } function saveSheet() { const data = collectData(); localStorage.setItem('fabulaUltimaSheet', JSON.stringify(data)); const st = document.getElementById('saveStatus'); st.classList.add('show'); setTimeout(() => st.classList.remove('show'), 2000); } function tryAutoLoad() { const raw = localStorage.getItem('fabulaUltimaSheet'); if (raw) try { applyData(JSON.parse(raw)); } catch (e) { } } function loadSheet() { const raw = localStorage.getItem('fabulaUltimaSheet'); if (!raw) return alert('No saved sheet found.'); try { applyData(JSON.parse(raw)); } catch (e) { alert('Could not load sheet.'); } } function applyData(d) { const set = (id, val) => { const el = document.getElementById(id); if (el && val !== undefined) el.value = val; }; set('charName', d.name); set('charPronouns', d.pronouns); set('charIdentity', d.identity); set('charTheme', d.theme); set('charOrigin', d.origin); set('charTraits', d.traits); level = d.level || 1; document.getElementById('levelDisplay').textContent = level; set('xpCurrent', d.xp); set('zenit', d.zenit); set('initMod', d.initMod); set('defense', d.defense); set('magDef', d.magDef); set('dex-base', d.dexBase); set('dex-cur', d.dexCur); set('ins-base', d.insBase); set('ins-cur', d.insCur); set('mig-base', d.migBase); set('mig-cur', d.migCur); set('wlp-base', d.wlpBase); set('wlp-cur', d.wlpCur); set('hpMax', d.hpMax); set('hpCur', d.hpCur); set('mpMax', d.mpMax); set('mpCur', d.mpCur); set('ipMax', d.ipMax); set('ipCur', d.ipCur); set('fpCount', d.fp); set('backpack', d.backpack); set('acc-name', d.accName); set('acc-desc', d.accDesc); set('arm-name', d.armName); set('arm-desc', d.armDesc); set('mh-name', d.mhName); set('mh-desc', d.mhDesc); set('oh-name', d.ohName); set('oh-desc', d.ohDesc); set('heroicSkills', d.heroicSkills); set('ritualsNotes', d.ritualsNotes); // Statuses document.querySelectorAll('.status-item').forEach(el => { const active = (d.statusesActive || []).includes(el.dataset.status); el.classList.toggle('active-status', active); el.querySelector('.status-check').textContent = active ? '✗' : ''; }); // Martial document.querySelectorAll('.martial-item').forEach(el => { const checked = (d.martialChecked || []).includes(el.querySelector('span').textContent); el.classList.toggle('checked', checked); el.querySelector('.martial-box').textContent = checked ? '✓' : ''; }); // Disciplines document.querySelectorAll('.disc-item').forEach(el => { const checked = (d.disciplinesChecked || []).includes(el.querySelector('span').textContent); el.classList.toggle('checked', checked); el.querySelector('.disc-box').textContent = checked ? '✓' : ''; }); if (d.bonds) { bonds = d.bonds; renderBonds(); } if (d.primaryClasses) { primaryClasses = d.primaryClasses; renderPrimaryClasses(); } if (d.otherClasses) { otherClasses = d.otherClasses; renderOtherClasses(); } if (d.spells) { spells = d.spells; renderSpells(); } renderFP(); updateXPBar(); updateCrisis(); } // ── EXPORT / IMPORT JSON ─────────────────────────── function exportSheet() { const data = collectData(); const json = JSON.stringify(data, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const charName = (data.name || 'character').replace(/[^a-z0-9_\- ]/gi, '').trim() || 'character'; a.href = url; a.download = charName + '-fabula-ultima.json'; a.click(); URL.revokeObjectURL(url); } function importSheet() { document.getElementById('importFileInput').value = ''; document.getElementById('importFileInput').click(); } function handleImportFile(input) { const file = input.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = e => { try { const data = JSON.parse(e.target.result); applyData(data); saveSheet(); const st = document.getElementById('saveStatus'); st.textContent = 'Imported!'; st.classList.add('show'); setTimeout(() => { st.classList.remove('show'); st.textContent = 'Saved!'; }, 2500); } catch (err) { alert('Could not import: invalid JSON file.'); } }; reader.readAsText(file); } // ── THEME ────────────────────────────────────────── function toggleTheme() { const html = document.documentElement; const goLight = html.dataset.theme !== 'light'; html.dataset.theme = goLight ? 'light' : 'dark'; document.getElementById('themeToggle').textContent = goLight ? '☾ Dark' : '☀ Light'; localStorage.setItem('fabulaUltimaTheme', html.dataset.theme); } // Auto-save every 30s setInterval(saveSheet, 30000); init();