From 9ade0820d6bbfe946baae651618997d0f188b53d Mon Sep 17 00:00:00 2001 From: Drew Malzahn Date: Fri, 5 Jun 2026 02:06:56 +0000 Subject: [PATCH] chore: Move JS to separate file --- Justfile | 1 + fabula-ultima-sheet.html | 368 +-------------------------------------- fabula-ultima-sheet.js | 365 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 367 insertions(+), 367 deletions(-) create mode 100644 fabula-ultima-sheet.js diff --git a/Justfile b/Justfile index 5ef1524..4940e70 100644 --- a/Justfile +++ b/Justfile @@ -8,3 +8,4 @@ serve: deploy: scp fabula-ultima-sheet.html {{ user }}@{{ host }}:{{ www-root }}/index.html scp fabula-ultima-sheet.css {{ user }}@{{ host }}:{{ www-root }}/fabula-ultima-sheet.css + scp fabula-ultima-sheet.js {{ user }}@{{ host }}:{{ www-root }}/fabula-ultima-sheet.js diff --git a/fabula-ultima-sheet.html b/fabula-ultima-sheet.html index 6247feb..69062b8 100644 --- a/fabula-ultima-sheet.html +++ b/fabula-ultima-sheet.html @@ -430,373 +430,7 @@ - + \ No newline at end of file diff --git a/fabula-ultima-sheet.js b/fabula-ultima-sheet.js new file mode 100644 index 0000000..39ecb6a --- /dev/null +++ b/fabula-ultima-sheet.js @@ -0,0 +1,365 @@ +// ── 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();