From 49f1419bc12335a9b39bf6a39d069db4eba20225 Mon Sep 17 00:00:00 2001 From: Drew Malzahn Date: Fri, 5 Jun 2026 01:42:05 +0000 Subject: [PATCH] chore: Formatting --- fabula-ultima-sheet.html | 2866 ++++++++++++++++++++++---------------- 1 file changed, 1629 insertions(+), 1237 deletions(-) diff --git a/fabula-ultima-sheet.html b/fabula-ultima-sheet.html index a8b5279..8ab8beb 100644 --- a/fabula-ultima-sheet.html +++ b/fabula-ultima-sheet.html @@ -1,1131 +1,1522 @@ + - - -Fabula Ultima — Character Sheet - - + .vital-inputs input { + text-align: center; + font-family: var(--font-mono); + font-size: 1.1rem; + } + + .vital-sep { + color: var(--text-dim); + font-size: 0.8rem; + flex-shrink: 0; + } + + .vital-formula { + font-size: 0.75rem; + color: var(--text-dim); + font-family: var(--font-mono); + } + + .crisis-badge { + font-family: var(--font-display); + font-size: 0.5rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--crisis); + border: 1px solid var(--crisis); + padding: 2px 6px; + flex-shrink: 0; + } + + /* ── LEVEL / XP ──────────────────────────────────── */ + .level-display { + text-align: center; + padding: 20px; + background: var(--surface2); + border: 1px solid var(--border); + } + + .level-num { + font-family: var(--font-display); + font-size: 3.5rem; + color: var(--gold); + line-height: 1; + display: block; + } + + .level-text { + font-family: var(--font-display); + font-size: 0.55rem; + letter-spacing: 0.25em; + color: var(--text-dim); + text-transform: uppercase; + display: block; + margin-top: 4px; + } + + .xp-bar-wrap { + margin-top: 14px; + background: var(--surface3); + border: 1px solid var(--border); + height: 8px; + position: relative; + overflow: hidden; + } + + .xp-bar { + height: 100%; + background: linear-gradient(90deg, var(--gold-dim), var(--gold)); + transition: width 0.5s ease; + } + + .xp-label { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--text-dim); + display: flex; + justify-content: space-between; + margin-top: 5px; + } + + /* ── FABULA POINTS ───────────────────────────────── */ + .fp-pips { + display: flex; + gap: 6px; + flex-wrap: wrap; + margin-top: 10px; + } + + .fp-pip { + width: 28px; + height: 28px; + border: 2px solid var(--border-bright); + border-radius: 50%; + cursor: pointer; + transition: all 0.15s; + background: var(--surface2); + display: flex; + align-items: center; + justify-content: center; + } + + .fp-pip.filled { + background: var(--teal); + border-color: var(--teal); + box-shadow: 0 0 8px rgba(78, 205, 196, 0.4); + } + + .fp-pip:hover { + border-color: var(--teal); + } + + .fp-rules { + margin-top: 14px; + border-top: 1px solid var(--border); + padding-top: 12px; + } + + .fp-rule { + font-size: 0.85rem; + color: var(--text-dim); + padding: 3px 0; + padding-left: 12px; + position: relative; + } + + .fp-rule::before { + content: '✦'; + position: absolute; + left: 0; + font-size: 0.5rem; + color: var(--gold); + top: 5px; + } + + .fp-rule strong { + color: var(--text); + } + + /* ── BONDS ───────────────────────────────────────── */ + .bond-block { + border: 1px solid var(--border); + padding: 14px; + background: var(--surface2); + margin-bottom: 10px; + } + + .bond-block:last-child { + margin-bottom: 0; + } + + .bond-header { + display: flex; + gap: 10px; + align-items: center; + margin-bottom: 10px; + } + + .bond-num { + font-family: var(--font-display); + font-size: 0.6rem; + color: var(--gold); + border: 1px solid var(--gold-dim); + width: 22px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + .bond-header input { + flex: 1; + } + + .bond-feelings { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 5px; + } + + .bond-feeling { + display: flex; + align-items: center; + gap: 5px; + cursor: pointer; + font-size: 0.8rem; + color: var(--text-dim); + user-select: none; + } + + .bond-feeling.active { + color: var(--teal); + } + + .bond-feeling-box { + width: 12px; + height: 12px; + border: 1px solid var(--border-bright); + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.6rem; + transition: all 0.15s; + } + + .bond-feeling.active .bond-feeling-box { + background: var(--teal); + border-color: var(--teal); + color: #000; + } + + /* ── EQUIPMENT ───────────────────────────────────── */ + .equip-row { + display: grid; + grid-template-columns: 120px 1fr; + gap: 0; + border-bottom: 1px solid var(--border); + } + + .equip-row:last-child { + border-bottom: none; + } + + .equip-slot { + font-family: var(--font-display); + font-size: 0.55rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--gold); + background: var(--surface3); + padding: 10px 12px; + border-right: 1px solid var(--border); + display: flex; + align-items: center; + } + + .equip-fields { + display: flex; + flex-direction: column; + gap: 0; + } + + .equip-fields input { + border: none; + border-bottom: 1px solid var(--border); + font-size: 0.9rem; + padding: 7px 10px; + } + + .equip-fields input:last-child { + border-bottom: none; + } + + .equip-fields input::placeholder { + color: var(--border-bright); + font-style: italic; + } + + /* ── MARTIAL CHECKBOXES ──────────────────────────── */ + .martial-row { + display: flex; + gap: 16px; + flex-wrap: wrap; + margin-top: 10px; + } + + .martial-item { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + user-select: none; + } + + .martial-box { + width: 14px; + height: 14px; + border: 1px solid var(--border-bright); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.65rem; + color: var(--teal); + transition: all 0.15s; + } + + .martial-item.checked .martial-box { + background: var(--teal-dim); + border-color: var(--teal); + } + + .martial-item span { + font-family: var(--font-display); + font-size: 0.55rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-dim); + } + + /* ── CLASSES ─────────────────────────────────────── */ + .class-block { + border: 1px solid var(--border); + background: var(--surface2); + margin-bottom: 12px; + } + + .class-block:last-child { + margin-bottom: 0; + } + + .class-header { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0; + border-bottom: 1px solid var(--border); + } + + .class-header input { + border: none; + border-right: 1px solid var(--border); + font-size: 0.9rem; + } + + .class-header input:last-child { + border-right: none; + } + + .class-skills { + padding: 0; + } + + .class-skills textarea { + border: none; + font-size: 0.85rem; + min-height: 80px; + background: transparent; + } + + /* ── SPELLS TABLE ────────────────────────────────── */ + .spells-table { + width: 100%; + border-collapse: collapse; + } + + .spells-table thead tr { + background: var(--surface3); + } + + .spells-table th { + font-family: var(--font-display); + font-size: 0.52rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--teal); + padding: 8px 10px; + text-align: left; + border-bottom: 1px solid var(--border-bright); + } + + .spells-table td { + border-bottom: 1px solid var(--border); + vertical-align: top; + } + + .spells-table td input, + .spells-table td textarea { + border: none; + font-size: 0.9rem; + min-height: 32px; + padding: 6px 8px; + } + + .spells-table td textarea { + min-height: 55px; + } + + .spell-name-col { + width: 35%; + } + + .spell-mp-col { + width: 12%; + } + + .spell-targets-col { + width: 18%; + } + + .spell-dur-col { + width: 18%; + } + + .spell-del-col { + width: 40px; + } + + .spell-del-btn { + background: none; + border: none; + color: var(--text-dim); + cursor: pointer; + padding: 6px 8px; + font-size: 0.9rem; + transition: color 0.15s; + } + + .spell-del-btn:hover { + color: var(--red); + } + + .add-btn { + margin-top: 8px; + font-family: var(--font-display); + font-size: 0.55rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--teal); + background: transparent; + border: 1px solid var(--teal-dim); + padding: 6px 14px; + cursor: pointer; + transition: all 0.15s; + display: inline-flex; + align-items: center; + gap: 6px; + } + + .add-btn:hover { + background: var(--teal-dim); + color: var(--text-bright); + } + + /* ── DISCIPLINES ─────────────────────────────────── */ + .disciplines-row { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 14px; + } + + .disc-item { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + user-select: none; + } + + .disc-box { + width: 13px; + height: 13px; + border: 1px solid var(--border-bright); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.6rem; + color: var(--gold); + transition: all 0.15s; + } + + .disc-item.checked .disc-box { + background: var(--gold-dim); + border-color: var(--gold); + } + + .disc-item span { + font-family: var(--font-display); + font-size: 0.55rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-dim); + } + + .disc-item.checked span { + color: var(--gold); + } + + /* ── SAVE / LOAD ─────────────────────────────────── */ + .toolbar { + display: flex; + gap: 8px; + align-items: center; + } + + .btn-save, + .btn-load { + font-family: var(--font-display); + font-size: 0.55rem; + letter-spacing: 0.15em; + text-transform: uppercase; + padding: 7px 16px; + cursor: pointer; + border: 1px solid var(--gold-dim); + background: transparent; + color: var(--gold); + transition: all 0.2s; + } + + .btn-save:hover { + background: var(--gold-dim); + color: var(--text-bright); + } + + .btn-load { + border-color: var(--border-bright); + color: var(--text-dim); + } + + .btn-load:hover { + border-color: var(--teal-dim); + color: var(--teal); + } + + .btn-export { + border-color: var(--teal-dim); + color: var(--teal); + } + + .btn-export:hover { + background: var(--teal-dim); + color: var(--text-bright); + } + + .btn-import { + border-color: var(--border-bright); + color: var(--text-dim); + } + + .btn-import:hover { + border-color: var(--gold-dim); + color: var(--gold); + } + + .toolbar-sep { + width: 1px; + height: 20px; + background: var(--border); + margin: 0 4px; + } + + .save-status { + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--teal); + opacity: 0; + transition: opacity 0.3s; + } + + .save-status.show { + opacity: 1; + } + + /* ── ORNAMENTS ───────────────────────────────────── */ + .ornament { + text-align: center; + color: var(--gold-dim); + font-size: 0.8rem; + letter-spacing: 0.5em; + margin: 8px 0; + } + + .def-row { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + margin-bottom: 14px; + } + + .def-block { + background: var(--surface2); + border: 1px solid var(--border); + padding: 10px 12px; + text-align: center; + } + + .def-block label { + display: block; + margin-bottom: 6px; + } + + .def-block input { + text-align: center; + font-family: var(--font-mono); + font-size: 1.2rem; + font-weight: 600; + } + + /* ── RESPONSIVE ──────────────────────────────────── */ + @media (max-width: 900px) { + + .grid-2, + .grid-3 { + grid-template-columns: 1fr; + } + + .col-span-2 { + grid-column: span 1; + } + + header { + flex-direction: column; + gap: 12px; + } + + .tabs { + flex-wrap: wrap; + } + } + + /* ── TOOLTIP ─────────────────────────────────────── */ + [title] { + cursor: help; + } + + /* scrollbar */ + ::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + ::-webkit-scrollbar-track { + background: var(--bg); + } + + ::-webkit-scrollbar-thumb { + background: var(--border-bright); + } + + ::-webkit-scrollbar-thumb:hover { + background: var(--teal-dim); + } + + -
- -
- - - -
-
- - -
- - - - Saved! -
-
+
+ +
+ + + +
+
+ + +
+ + + + Saved! +
+
- -
+
- -
+ +
- -
-
Identity & Traits
-
-
- - + +
+
Identity & Traits
+
+
+ + +
+
+ + +
- - + +
-
-
- - -
-
-
- - +
+
+ + +
+
+ + +
- - + +
-
- - + + +
+ +
+
Level & Experience
+
+
+ 1 + Character Level + + +
+
+
+ + +
+
+
+
+
+ 0 XP + 10 XP = Level +
+
+ + +
+
+
+
+ +
+
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
-
-
- - -
-
- - -
-
- - -
-
+
Bonds
+
-
- -
+ +
- -
-
Attributes
-
-
-
Dexterity
-
-
- - -
-
-
- - -
+ +
+
Equipment
+
+
+
Martial Armor +
+
+
Martial Shields +
+
+
Martial Melee +
+
+
Martial Ranged
-
-
Insight
-
-
- - -
-
-
- - +
+
+
Accessory
+
+ +
-
-
-
Might
-
-
- - -
-
-
- - +
+
Armor
+
+ +
-
-
-
Willpower
-
-
- - +
+
Main Hand
+
+ +
-
-
- - +
+
+
Off-Hand
+
+ +
-
- -
-
Status Effects
-
- -
-
- - -
-
Hit, Mind & Inventory Points
- -
-
HP
-
-
MIG×5 + Level + Other
-
- -
/
- - -
-
- + +
+
Backpack & Notes
+
-
-
MP
-
-
WLP×5 + Level + Other
-
- -
/
- -
-
- -
- -
-
IP
-
-
6 + Other
-
- -
/
- -
-
-
-
-
- - -
- - -
-
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)
+
+ +
+ +
+
Other Classes (max 3 non-mastered)
+
+ +
-
-
Primary Classes (up to 3 levels each)
-
-
-
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 +
+
+ +
+
-
+ + // ── 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); + } + + // Auto-save every 30s + setInterval(saveSheet, 30000); + + init(); + - + + \ No newline at end of file