diff --git a/fabula-ultima-sheet.css b/fabula-ultima-sheet.css index 2e558d3..3bb6724 100644 --- a/fabula-ultima-sheet.css +++ b/fabula-ultima-sheet.css @@ -14,9 +14,9 @@ --text-dim: #7a8a82; --text-bright: #ede8dc; --crisis: #e74c3c; - --font-display: 'Cinzel', serif; - --font-body: 'Crimson Text', serif; - --font-mono: 'Inconsolata', monospace; + --font-display: "Cinzel", serif; + --font-body: "Crimson Text", serif; + --font-mono: "Inconsolata", monospace; } * { @@ -32,8 +32,16 @@ body { font-size: 16px; min-height: 100vh; background-image: - radial-gradient(ellipse at 20% 10%, rgba(78, 205, 196, 0.04) 0%, transparent 50%), - radial-gradient(ellipse at 80% 90%, rgba(201, 168, 76, 0.04) 0%, transparent 50%); + radial-gradient( + ellipse at 20% 10%, + rgba(78, 205, 196, 0.04) 0%, + transparent 50% + ), + radial-gradient( + ellipse at 80% 90%, + rgba(201, 168, 76, 0.04) 0%, + transparent 50% + ); } /* ── HEADER ─────────────────────────────────────── */ @@ -62,7 +70,7 @@ header { } .logo::before { - content: '✦'; + content: "✦"; color: var(--gold); font-size: 0.9em; } @@ -146,7 +154,7 @@ header { } .section::before { - content: ''; + content: ""; position: absolute; top: 0; left: 0; @@ -204,7 +212,9 @@ select { font-size: 1rem; padding: 8px 11px; outline: none; - transition: border-color 0.2s, box-shadow 0.2s; + transition: + border-color 0.2s, + box-shadow 0.2s; -webkit-appearance: none; } @@ -502,7 +512,7 @@ input[type="number"] { } .fp-rule::before { - content: '✦'; + content: "✦"; position: absolute; left: 0; font-size: 0.5rem; @@ -1034,8 +1044,16 @@ input[type="number"] { [data-theme="light"] body { background-image: - radial-gradient(ellipse at 20% 10%, rgba(26, 122, 118, 0.07) 0%, transparent 50%), - radial-gradient(ellipse at 80% 90%, rgba(122, 90, 24, 0.07) 0%, transparent 50%); + radial-gradient( + ellipse at 20% 10%, + rgba(26, 122, 118, 0.07) 0%, + transparent 50% + ), + radial-gradient( + ellipse at 80% 90%, + rgba(122, 90, 24, 0.07) 0%, + transparent 50% + ); } [data-theme="light"] header { @@ -1075,7 +1093,6 @@ input[type="number"] { /* ── RESPONSIVE ──────────────────────────────────── */ @media (max-width: 900px) { - .grid-2, .grid-3 { grid-template-columns: 1fr; diff --git a/fabula-ultima-sheet.html b/fabula-ultima-sheet.html index 3a86b25..3798f55 100644 --- a/fabula-ultima-sheet.html +++ b/fabula-ultima-sheet.html @@ -1,473 +1,690 @@ - + + + + + Fabula Ultima — Character Sheet + + + + - - - - Fabula Ultima — Character Sheet - - - - + +
+ +
+ + + + +
+
+ Saved! + +
+
- - -
- -
- - - - + -
- Saved! - -
-
- - - -
+
+ +
+ +
+
+ Identity & Traits +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
- -
+ +
+
+
+ Level & Experience +
+
+
+ 1 + Character Level + + +
+
+
+ + +
+
+
+
+
+ 0 XP + 10 XP = Level +
+
+ + +
+
+
+
- -
-
Identity & Traits
-
-
- - +
+
+ Defenses +
+
+
+ + +
+
+ + +
+
+ + +
+
-
- - -
-
-
- - -
-
-
- - -
-
- - -
-
-
- -
- -
- + +
+
-
Level & Experience
-
-
- 1 - Character Level - - +
+ Attributes +
+
+
+
Dexterity
+
+
+ + +
+
+
+ + +
+
+
+
Insight
+
+
+ + +
+
+
+ + +
+
+
+
+
Might
+
+
+ + +
+
+
+ + +
+
+
+
+
Willpower
+
+
+ + +
+
+
+ + +
+
+
+
+
+ + +
+
+ Status Effects +
+
+ +
+
+ + +
+
+ Hit, Mind & Inventory Points +
+ +
+
HP
+
+
MIG×5 + Level + Other
+
+ +
/
+ + +
+
+ +
+ +
+
MP
+
+
WLP×5 + Level + Other
+
+ +
/
+ +
+
+ +
+ +
+
IP
+
+
6 + Other
+
+ +
/
+ +
+
+
+
+
+ + +
+ +
+
+ Fabula Points +
+
-
- - -
-
-
-
-
- 0 XP - 10 XP = Level -
-
- - -
+ + +
+
+
+
+
+ +1 FP if you have none at start of session. +
+
+ +1 FP when a Villain makes an entrance. +
+
+ +1 FP when you fumble a Check. +
+
+ +2 FP if you surrender at zero HP. +
+
+ Spend 1 FP to invoke a trait: reroll one or both + dice. +
+
+ Spend 1 FP to invoke a bond: add its strength to + the result. +
+
+ Spend 1 FP to alter the story.
+
-
Defenses
-
-
- - -
-
- - -
-
- - -
-
-
- -
-
- - -
- - -
-
Attributes
-
-
-
Dexterity
-
-
- - -
-
-
- - -
-
-
-
-
Insight
-
-
- - -
-
-
- - -
-
-
-
-
Might
-
-
- - -
-
-
- - -
-
-
-
-
Willpower
-
-
- - -
-
-
- - -
-
-
+
Bonds
+
- -
-
Status Effects
-
- -
-
- - -
-
Hit, Mind & Inventory Points
- -
-
HP
-
-
MIG×5 + Level + Other
-
- -
/
- - + +
+ +
+
Equipment
+
+
+
+ Martial Armor +
+
+
+ Martial Shields +
+
+
+ Martial Melee +
+
+
+ Martial Ranged
- -
- -
-
MP
-
-
WLP×5 + Level + Other
-
- -
/
- +
+
+
Accessory
+
+ + +
+
+
+
Armor
+
+ + +
+
+
+
Main Hand
+
+ + +
+
+
+
Off-Hand
+
+ + +
-
-
-
IP
-
-
6 + Other
-
- -
/
- -
+ +
+
+ Backpack & Notes
+
- -
- - -
-
Fabula Points
-
-
- - -
-
-
-
-
+1 FP if you have none at start of session.
-
+1 FP when a Villain makes an entrance.
-
+1 FP when you fumble a Check.
-
+2 FP if you surrender at zero HP.
-
Spend 1 FP to invoke a trait: reroll one or both - dice.
-
Spend 1 FP to invoke a bond: add its strength to the result.
-
Spend 1 FP to alter the story.
-
-
- - -
-
Bonds
-
-
- -
- - -
- - -
-
Equipment
-
-
-
Martial Armor -
-
-
Martial Shields -
-
-
Martial Melee -
-
-
Martial Ranged -
-
-
-
-
Accessory
-
- - -
-
-
-
Armor
-
- - -
-
-
-
Main Hand
-
- - -
-
-
-
Off-Hand
-
- - -
-
-
-
- - -
-
Backpack & Notes
- -
- -
- -
- - -
+
+
+
+
+ Primary Classes (up to 3 levels each) +
+
+ +
-
- -
-
Primary Classes (up to 3 levels each)
-
- +
+
+ Other Classes (max 3 non-mastered) +
+
+ +
-
Other Classes (max 3 non-mastered)
-
- +
+ Heroic Skills +
+
-
-
-
Heroic Skills
- -
- -
- - -
- -
-
Arcana & Spells
- - - - - - - - - - - -
Name / NotesMP CostTargetsDuration
- -
- -
-
Rituals
-
-
-
Arcanism -
-
-
Chimerism -
-
-
Elementalism -
-
-
Entropism -
-
-
Ritualism -
-
-
Spiritism +
+
+
+ Arcana & Spells
+ + + + + + + + + + + +
Name / NotesMP CostTargetsDuration
+ +
+ +
+
Rituals
+
+
+
+ Arcanism +
+
+
+ Chimerism +
+
+
+ Elementalism +
+
+
+ Entropism +
+
+
+ Ritualism +
+
+
+ Spiritism +
+
+
-
-
- - -
-
+
+
+
+
+ Local Save +
+

+ Save your character sheet to your browser's local storage, or load a + previously saved sheet. +

+
+ + +
+
-
-
Local Save
-

Save your character sheet to your browser's local storage, or load a previously saved sheet.

-
- - +
+
JSON File
+

+ Export your character to a JSON file for backup or sharing, or + import from a previously exported file. +

+
+ + + +
+
+ +
+
+ Share via URL +
+

+ Encode your character's current state into a shareable link. Anyone + who opens the link will see your character — auto-save is disabled + for viewers. +

+
+ + Copied! +
- -
-
JSON File
-

Export your character to a JSON file for backup or sharing, or import from a previously exported file.

-
- - - -
-
- -
-
Share via URL
-

Encode your character's current state into a shareable link. Anyone who opens the link will see your character — auto-save is disabled for viewers.

-
- - Copied! -
-
-
-
- - - - \ No newline at end of file + + + diff --git a/fabula-ultima-sheet.js b/fabula-ultima-sheet.js index 10b3f21..0ea2968 100644 --- a/fabula-ultima-sheet.js +++ b/fabula-ultima-sheet.js @@ -6,30 +6,45 @@ let primaryClasses = []; let otherClasses = []; let spells = []; let bonds = [ - { name: '', feelings: [] }, - { name: '', feelings: [] }, - { name: '', feelings: [] }, - { name: '', feelings: [] }, - { name: '', feelings: [] }, - { name: '', feelings: [] }, + { 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']; +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'); + 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 ─────────────────────────────────────────── async function init() { - const savedTheme = localStorage.getItem('fabulaUltimaTheme') - || (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'); - document.getElementById('themeToggle').textContent = savedTheme === 'light' ? '☾ Dark' : '☀ Light'; + 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(); @@ -37,26 +52,27 @@ async function init() { renderOtherClasses(); renderSpells(); updateXPBar(); - if (!await tryLoadFromURL()) tryAutoLoad(); + if (!(await tryLoadFromURL())) tryAutoLoad(); } // ── URL STATE ────────────────────────────────────── async function compressToBase64(str) { - const stream = new CompressionStream('deflate-raw'); + const stream = new CompressionStream("deflate-raw"); const writer = stream.writable.getWriter(); writer.write(new TextEncoder().encode(str)); writer.close(); const buf = await new Response(stream.readable).arrayBuffer(); const bytes = new Uint8Array(buf); - let binary = ''; - for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); + let binary = ""; + for (let i = 0; i < bytes.length; i++) + binary += String.fromCharCode(bytes[i]); return btoa(binary); } async function decompressFromBase64(b64) { try { - const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0)); - const stream = new DecompressionStream('deflate-raw'); + const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); + const stream = new DecompressionStream("deflate-raw"); const writer = stream.writable.getWriter(); writer.write(bytes); writer.close(); @@ -69,29 +85,36 @@ async function decompressFromBase64(b64) { } async function tryLoadFromURL() { - const encoded = new URLSearchParams(window.location.search).get('c'); + const encoded = new URLSearchParams(window.location.search).get("c"); if (!encoded) return false; try { const json = await decompressFromBase64(encoded); applyData(JSON.parse(json)); urlMode = true; - document.getElementById('urlBanner').style.display = 'block'; + document.getElementById("urlBanner").style.display = "block"; return true; } catch (e) { - console.warn('Could not load state from URL:', e); + console.warn("Could not load state from URL:", e); return false; } } // ── STATUS EFFECTS ───────────────────────────────── function renderStatuses() { - const g = document.getElementById('statusGrid'); - g.innerHTML = ''; - STATUSES.forEach(s => { - const el = document.createElement('div'); - el.className = 'status-item'; + 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.onclick = () => { + el.classList.toggle("active-status"); + el.querySelector(".status-check").textContent = el.classList.contains( + "active-status", + ) + ? "✗" + : ""; + }; el.innerHTML = `
${s}
`; g.appendChild(el); }); @@ -99,16 +122,16 @@ function renderStatuses() { // ── FABULA POINTS ────────────────────────────────── function renderFP() { - fpCount = parseInt(document.getElementById('fpCount').value) || 0; - const pips = document.getElementById('fpPips'); + fpCount = parseInt(document.getElementById("fpCount").value) || 0; + const pips = document.getElementById("fpPips"); const total = Math.max(10, fpCount); - pips.innerHTML = ''; + pips.innerHTML = ""; for (let i = 0; i < total; i++) { - const pip = document.createElement('div'); - pip.className = 'fp-pip' + (i < fpCount ? ' filled' : ''); + 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; + document.getElementById("fpCount").value = newVal; renderFP(); }; pips.appendChild(pip); @@ -117,22 +140,24 @@ function renderFP() { // ── BONDS ────────────────────────────────────────── function renderBonds() { - const container = document.getElementById('bondsContainer'); - container.innerHTML = ''; + const container = document.getElementById("bondsContainer"); + container.innerHTML = ""; bonds.forEach((bond, idx) => { - const el = document.createElement('div'); - el.className = 'bond-block'; + const el = document.createElement("div"); + el.className = "bond-block"; el.innerHTML = `
${idx + 1}
- ${FEELINGS.map(f => ` -
-
${bond.feelings.includes(f) ? '✓' : ''}
+ ${FEELINGS.map( + (f) => ` +
+
${bond.feelings.includes(f) ? "✓" : ""}
${f} -
`).join('')} +
`, + ).join("")}
`; container.appendChild(el); }); @@ -141,71 +166,88 @@ function renderBonds() { 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 = ''; + 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 = '✓'; + 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; + document.getElementById("levelDisplay").textContent = level; } // ── XP BAR ───────────────────────────────────────── function updateXPBar() { - const xp = parseInt(document.getElementById('xpCurrent').value) || 0; + 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'; + document.getElementById("xpBar").style.width = pct + "%"; + document.getElementById("xpVal").textContent = xp + " XP"; } // ── STAT CALC ────────────────────────────────────── -function recalcStats() { /* auto-calc on keyup */ } +function recalcStats() { + /* auto-calc on keyup */ +} function calcHP() { - const mig = parseInt(document.getElementById('mig-base').value) || 6; + 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; + 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 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; + 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'; + 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') ? '✓' : ''; } +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') ? '✓' : ''; } +function toggleDisc(el) { + el.classList.toggle("checked"); + el.querySelector(".disc-box").textContent = el.classList.contains("checked") + ? "✓" + : ""; +} // ── CLASSES ──────────────────────────────────────── function classBlockHTML(cls, idx, type) { return `
- - + +
- +
@@ -214,38 +256,63 @@ function classBlockHTML(cls, idx, type) { } function renderPrimaryClasses() { - document.getElementById('primaryClassContainer').innerHTML = primaryClasses.map((c, i) => classBlockHTML(c, i, 'primary')).join(''); + 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(''); + document.getElementById("otherClassContainer").innerHTML = otherClasses + .map((c, i) => classBlockHTML(c, i, "other")) + .join(""); } -function addPrimaryClass() { primaryClasses.push({}); renderPrimaryClasses(); } -function addOtherClass() { otherClasses.push({}); renderOtherClasses(); } +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(); } + 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) => ` + const tbody = document.getElementById("spellsBody"); + tbody.innerHTML = spells + .map( + (s, i) => ` - - + + - - - + + + - `).join(''); + `, + ) + .join(""); } -function addSpell() { spells.push({}); renderSpells(); } -function removeSpell(i) { spells.splice(i, 1); renderSpells(); } +function addSpell() { + spells.push({}); + renderSpells(); +} +function removeSpell(i) { + spells.splice(i, 1); + renderSpells(); +} // ── SAVE / LOAD ──────────────────────────────────── // Short key legend (serialized form only; in-memory objects use full names): @@ -265,128 +332,206 @@ function removeSpell(i) { spells.splice(i, 1); renderSpells(); } // Nested classes: n=name, b=benefits, s=skills // Nested spells: n=name, nt=notes, mp=mp, tg=targets, dr=duration function collectData() { - const get = id => { const el = document.getElementById(id); return el ? el.value : ''; }; - const sa = [...document.querySelectorAll('.status-item.active-status')].map(el => el.dataset.status); - const ma = [...document.querySelectorAll('.martial-item.checked')].map(el => el.querySelector('span').textContent); - const da = [...document.querySelectorAll('.disc-item.checked')].map(el => el.querySelector('span').textContent); + const get = (id) => { + const el = document.getElementById(id); + return el ? el.value : ""; + }; + const sa = [...document.querySelectorAll(".status-item.active-status")].map( + (el) => el.dataset.status, + ); + const ma = [...document.querySelectorAll(".martial-item.checked")].map( + (el) => el.querySelector("span").textContent, + ); + const da = [...document.querySelectorAll(".disc-item.checked")].map( + (el) => el.querySelector("span").textContent, + ); return { - n: get('charName'), pn: get('charPronouns'), - id: get('charIdentity'), th: get('charTheme'), og: get('charOrigin'), - tr: get('charTraits'), - lv: level, xp: get('xpCurrent'), z: get('zenit'), - im: get('initMod'), df: get('defense'), md: get('magDef'), - dxb: get('dex-base'), dxc: get('dex-cur'), - inb: get('ins-base'), inc: get('ins-cur'), - mgb: get('mig-base'), mgc: get('mig-cur'), - wpb: get('wlp-base'), wpc: get('wlp-cur'), - hx: get('hpMax'), hc: get('hpCur'), - mx: get('mpMax'), mc: get('mpCur'), - ix: get('ipMax'), ic: get('ipCur'), - fp: get('fpCount'), - bp: get('backpack'), - acn: get('acc-name'), acd: get('acc-desc'), - amn: get('arm-name'), amd: get('arm-desc'), - mhn: get('mh-name'), mhd: get('mh-desc'), - ohn: get('oh-name'), ohd: get('oh-desc'), - hs: get('heroicSkills'), - rn: get('ritualsNotes'), - sa, ma, da, - bo: bonds.map(b => ({ n: b.name, f: b.feelings })), - pc: primaryClasses.map(c => ({ n: c.name, b: c.benefits, s: c.skills })), - oc: otherClasses.map(c => ({ n: c.name, b: c.benefits, s: c.skills })), - sp: spells.map(s => ({ n: s.name, nt: s.notes, mp: s.mp, tg: s.targets, dr: s.duration })), + n: get("charName"), + pn: get("charPronouns"), + id: get("charIdentity"), + th: get("charTheme"), + og: get("charOrigin"), + tr: get("charTraits"), + lv: level, + xp: get("xpCurrent"), + z: get("zenit"), + im: get("initMod"), + df: get("defense"), + md: get("magDef"), + dxb: get("dex-base"), + dxc: get("dex-cur"), + inb: get("ins-base"), + inc: get("ins-cur"), + mgb: get("mig-base"), + mgc: get("mig-cur"), + wpb: get("wlp-base"), + wpc: get("wlp-cur"), + hx: get("hpMax"), + hc: get("hpCur"), + mx: get("mpMax"), + mc: get("mpCur"), + ix: get("ipMax"), + ic: get("ipCur"), + fp: get("fpCount"), + bp: get("backpack"), + acn: get("acc-name"), + acd: get("acc-desc"), + amn: get("arm-name"), + amd: get("arm-desc"), + mhn: get("mh-name"), + mhd: get("mh-desc"), + ohn: get("oh-name"), + ohd: get("oh-desc"), + hs: get("heroicSkills"), + rn: get("ritualsNotes"), + sa, + ma, + da, + bo: bonds.map((b) => ({ n: b.name, f: b.feelings })), + pc: primaryClasses.map((c) => ({ n: c.name, b: c.benefits, s: c.skills })), + oc: otherClasses.map((c) => ({ n: c.name, b: c.benefits, s: c.skills })), + sp: spells.map((s) => ({ + n: s.name, + nt: s.notes, + mp: s.mp, + tg: s.targets, + dr: s.duration, + })), }; } function saveSheet(isAuto = false) { if (isAuto && urlMode) return; const data = collectData(); - localStorage.setItem('fabulaUltimaSheet', JSON.stringify(data)); - const st = document.getElementById('saveStatus'); - st.classList.add('show'); - setTimeout(() => st.classList.remove('show'), 2000); + 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) { } + 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.'); } + 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.n ?? d.name); set('charPronouns', d.pn ?? d.pronouns); - set('charIdentity',d.id ?? d.identity); set('charTheme', d.th ?? d.theme); - set('charOrigin', d.og ?? d.origin); set('charTraits', d.tr ?? d.traits); + const set = (id, val) => { + const el = document.getElementById(id); + if (el && val !== undefined) el.value = val; + }; + set("charName", d.n ?? d.name); + set("charPronouns", d.pn ?? d.pronouns); + set("charIdentity", d.id ?? d.identity); + set("charTheme", d.th ?? d.theme); + set("charOrigin", d.og ?? d.origin); + set("charTraits", d.tr ?? d.traits); level = d.lv ?? d.level ?? 1; - document.getElementById('levelDisplay').textContent = level; - set('xpCurrent', d.xp); set('zenit', d.z ?? d.zenit); - set('initMod', d.im ?? d.initMod); set('defense', d.df ?? d.defense); - set('magDef', d.md ?? d.magDef); - set('dex-base', d.dxb ?? d.dexBase); set('dex-cur', d.dxc ?? d.dexCur); - set('ins-base', d.inb ?? d.insBase); set('ins-cur', d.inc ?? d.insCur); - set('mig-base', d.mgb ?? d.migBase); set('mig-cur', d.mgc ?? d.migCur); - set('wlp-base', d.wpb ?? d.wlpBase); set('wlp-cur', d.wpc ?? d.wlpCur); - set('hpMax', d.hx ?? d.hpMax); set('hpCur', d.hc ?? d.hpCur); - set('mpMax', d.mx ?? d.mpMax); set('mpCur', d.mc ?? d.mpCur); - set('ipMax', d.ix ?? d.ipMax); set('ipCur', d.ic ?? d.ipCur); - set('fpCount', d.fp); - set('backpack', d.bp ?? d.backpack); - set('acc-name', d.acn ?? d.accName); set('acc-desc', d.acd ?? d.accDesc); - set('arm-name', d.amn ?? d.armName); set('arm-desc', d.amd ?? d.armDesc); - set('mh-name', d.mhn ?? d.mhName); set('mh-desc', d.mhd ?? d.mhDesc); - set('oh-name', d.ohn ?? d.ohName); set('oh-desc', d.ohd ?? d.ohDesc); - set('heroicSkills', d.hs ?? d.heroicSkills); - set('ritualsNotes', d.rn ?? d.ritualsNotes); + document.getElementById("levelDisplay").textContent = level; + set("xpCurrent", d.xp); + set("zenit", d.z ?? d.zenit); + set("initMod", d.im ?? d.initMod); + set("defense", d.df ?? d.defense); + set("magDef", d.md ?? d.magDef); + set("dex-base", d.dxb ?? d.dexBase); + set("dex-cur", d.dxc ?? d.dexCur); + set("ins-base", d.inb ?? d.insBase); + set("ins-cur", d.inc ?? d.insCur); + set("mig-base", d.mgb ?? d.migBase); + set("mig-cur", d.mgc ?? d.migCur); + set("wlp-base", d.wpb ?? d.wlpBase); + set("wlp-cur", d.wpc ?? d.wlpCur); + set("hpMax", d.hx ?? d.hpMax); + set("hpCur", d.hc ?? d.hpCur); + set("mpMax", d.mx ?? d.mpMax); + set("mpCur", d.mc ?? d.mpCur); + set("ipMax", d.ix ?? d.ipMax); + set("ipCur", d.ic ?? d.ipCur); + set("fpCount", d.fp); + set("backpack", d.bp ?? d.backpack); + set("acc-name", d.acn ?? d.accName); + set("acc-desc", d.acd ?? d.accDesc); + set("arm-name", d.amn ?? d.armName); + set("arm-desc", d.amd ?? d.armDesc); + set("mh-name", d.mhn ?? d.mhName); + set("mh-desc", d.mhd ?? d.mhDesc); + set("oh-name", d.ohn ?? d.ohName); + set("oh-desc", d.ohd ?? d.ohDesc); + set("heroicSkills", d.hs ?? d.heroicSkills); + set("ritualsNotes", d.rn ?? d.ritualsNotes); // Statuses const sa = d.sa ?? d.statusesActive ?? []; - document.querySelectorAll('.status-item').forEach(el => { + document.querySelectorAll(".status-item").forEach((el) => { const active = sa.includes(el.dataset.status); - el.classList.toggle('active-status', active); - el.querySelector('.status-check').textContent = active ? '✗' : ''; + el.classList.toggle("active-status", active); + el.querySelector(".status-check").textContent = active ? "✗" : ""; }); // Martial const ma = d.ma ?? d.martialChecked ?? []; - document.querySelectorAll('.martial-item').forEach(el => { - const checked = ma.includes(el.querySelector('span').textContent); - el.classList.toggle('checked', checked); - el.querySelector('.martial-box').textContent = checked ? '✓' : ''; + document.querySelectorAll(".martial-item").forEach((el) => { + const checked = ma.includes(el.querySelector("span").textContent); + el.classList.toggle("checked", checked); + el.querySelector(".martial-box").textContent = checked ? "✓" : ""; }); // Disciplines const da = d.da ?? d.disciplinesChecked ?? []; - document.querySelectorAll('.disc-item').forEach(el => { - const checked = da.includes(el.querySelector('span').textContent); - el.classList.toggle('checked', checked); - el.querySelector('.disc-box').textContent = checked ? '✓' : ''; + document.querySelectorAll(".disc-item").forEach((el) => { + const checked = da.includes(el.querySelector("span").textContent); + el.classList.toggle("checked", checked); + el.querySelector(".disc-box").textContent = checked ? "✓" : ""; }); const rawBonds = d.bo ?? d.bonds; if (rawBonds) { - bonds = rawBonds.map(b => ({ name: b.n ?? b.name, feelings: b.f ?? b.feelings ?? [] })); + bonds = rawBonds.map((b) => ({ + name: b.n ?? b.name, + feelings: b.f ?? b.feelings ?? [], + })); renderBonds(); } const rawPrimary = d.pc ?? d.primaryClasses; if (rawPrimary) { - primaryClasses = rawPrimary.map(c => ({ name: c.n ?? c.name, benefits: c.b ?? c.benefits, skills: c.s ?? c.skills })); + primaryClasses = rawPrimary.map((c) => ({ + name: c.n ?? c.name, + benefits: c.b ?? c.benefits, + skills: c.s ?? c.skills, + })); renderPrimaryClasses(); } const rawOther = d.oc ?? d.otherClasses; if (rawOther) { - otherClasses = rawOther.map(c => ({ name: c.n ?? c.name, benefits: c.b ?? c.benefits, skills: c.s ?? c.skills })); + otherClasses = rawOther.map((c) => ({ + name: c.n ?? c.name, + benefits: c.b ?? c.benefits, + skills: c.s ?? c.skills, + })); renderOtherClasses(); } const rawSpells = d.sp ?? d.spells; if (rawSpells) { - spells = rawSpells.map(s => ({ name: s.n ?? s.name, notes: s.nt ?? s.notes, mp: s.mp, targets: s.tg ?? s.targets, duration: s.dr ?? s.duration })); + spells = rawSpells.map((s) => ({ + name: s.n ?? s.name, + notes: s.nt ?? s.notes, + mp: s.mp, + targets: s.tg ?? s.targets, + duration: s.dr ?? s.duration, + })); renderSpells(); } @@ -399,36 +544,41 @@ function applyData(d) { function exportSheet() { const data = collectData(); const json = JSON.stringify(data, null, 2); - const blob = new Blob([json], { type: 'application/json' }); + const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - const charName = (data.n || 'character').replace(/[^a-z0-9_\- ]/gi, '').trim() || 'character'; + const a = document.createElement("a"); + const charName = + (data.n || "character").replace(/[^a-z0-9_\- ]/gi, "").trim() || + "character"; a.href = url; - a.download = charName + '-fabula-ultima.json'; + a.download = charName + "-fabula-ultima.json"; a.click(); URL.revokeObjectURL(url); } function importSheet() { - document.getElementById('importFileInput').value = ''; - document.getElementById('importFileInput').click(); + 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 => { + 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); + 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.'); + alert("Could not import: invalid JSON file."); } }; reader.readAsText(file); @@ -437,20 +587,26 @@ function handleImportFile(input) { async function copyShareURL() { const json = JSON.stringify(collectData()); const encoded = await compressToBase64(json); - const url = window.location.origin + window.location.pathname + '?c=' + encodeURIComponent(encoded); + const url = + window.location.origin + + window.location.pathname + + "?c=" + + encodeURIComponent(encoded); await navigator.clipboard.writeText(url); - const st = document.getElementById('copyStatus'); - st.classList.add('show'); - setTimeout(() => st.classList.remove('show'), 2000); + const st = document.getElementById("copyStatus"); + st.classList.add("show"); + setTimeout(() => st.classList.remove("show"), 2000); } // ── 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); + 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 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5d722a1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,27 @@ +{ + "name": "fabula-ultima-html", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "prettier": "^3.8.3" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3e5346a --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "prettier": "^3.8.3" + } +}