- Move Save, Load, Export, Import buttons to a dedicated Manage page - Add light theme with warm parchment palette; auto-detect via prefers-color-scheme, persisted to localStorage - Add URL sharing: sheet state is deflate-raw compressed, base64-encoded, and stored in ?c= parameter; auto-save is suppressed when viewing a shared URL - Extract JS to fabula-ultima-sheet.js Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
473 lines
20 KiB
HTML
473 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en" data-theme="dark">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Fabula Ultima — Character Sheet</title>
|
||
<link
|
||
href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Crimson+Text:ital,wght@0,400;0,600;1,400&family=Inconsolata:wght@400;600&display=swap"
|
||
rel="stylesheet">
|
||
<link rel="stylesheet" href="fabula-ultima-sheet.css">
|
||
<script>(function () { var t = localStorage.getItem('fabulaUltimaTheme') || (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'); document.documentElement.dataset.theme = t; })()</script>
|
||
</head>
|
||
|
||
<body>
|
||
|
||
<header>
|
||
<div class="logo">Fabula Ultima</div>
|
||
<div class="tabs">
|
||
<button class="tab active" onclick="switchTab('main')">Character</button>
|
||
<button class="tab" onclick="switchTab('classes')">Classes</button>
|
||
<button class="tab" onclick="switchTab('spells')">Arcana & Spells</button>
|
||
<button class="tab" onclick="switchTab('manage')">Manage</button>
|
||
</div>
|
||
<div class="toolbar">
|
||
<span class="save-status" id="saveStatus">Saved!</span>
|
||
<button class="btn-theme" id="themeToggle" onclick="toggleTheme()">☀ Light</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div id="urlBanner" class="url-banner" style="display:none">
|
||
<span class="url-banner-icon">⚑</span>
|
||
Viewing a shared character — auto-save is disabled.
|
||
Use <strong>Manage → Save to Browser</strong> to keep any changes.
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════════════════
|
||
PAGE 1: MAIN CHARACTER
|
||
══════════════════════════════════════════════════ -->
|
||
<div class="page active" id="page-main">
|
||
|
||
<!-- Row 1: Identity + Level -->
|
||
<div class="grid-2" style="margin-bottom:20px;">
|
||
|
||
<!-- Identity -->
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">✦</span> Identity & Traits</div>
|
||
<div class="field-row">
|
||
<div class="field" style="flex:2">
|
||
<label>Name</label>
|
||
<input type="text" id="charName" placeholder="Character name…">
|
||
</div>
|
||
<div class="field">
|
||
<label>Pronouns</label>
|
||
<input type="text" id="charPronouns" placeholder="they/them">
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<label>Identity</label>
|
||
<input type="text" id="charIdentity" placeholder="Who are you?">
|
||
</div>
|
||
<div class="field-row">
|
||
<div class="field">
|
||
<label>Theme</label>
|
||
<input type="text" id="charTheme" placeholder="Your theme…">
|
||
</div>
|
||
<div class="field">
|
||
<label>Origin</label>
|
||
<input type="text" id="charOrigin" placeholder="Where from?">
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<label>Traits (comma-separated)</label>
|
||
<textarea id="charTraits" placeholder="Brave, Reckless, Loyal to a fault…"
|
||
style="min-height:55px;"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Level + XP + Defenses -->
|
||
<div style="display:flex; flex-direction:column; gap:16px;">
|
||
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">⬡</span> Level & Experience</div>
|
||
<div class="grid-2" style="gap:14px;">
|
||
<div class="level-display">
|
||
<span class="level-num" id="levelDisplay">1</span>
|
||
<span class="level-text">Character Level</span>
|
||
<button class="add-btn" style="margin-top:10px; width:100%; justify-content:center;"
|
||
onclick="adjustLevel(1)">+ Level Up</button>
|
||
<button class="add-btn"
|
||
style="margin-top:4px; width:100%; justify-content:center; border-color:var(--border-bright); color:var(--text-dim);"
|
||
onclick="adjustLevel(-1)">− Level Down</button>
|
||
</div>
|
||
<div>
|
||
<div class="field">
|
||
<label>Experience Points (XP)</label>
|
||
<input type="number" id="xpCurrent" value="0" min="0" oninput="updateXPBar()">
|
||
</div>
|
||
<div class="xp-bar-wrap">
|
||
<div class="xp-bar" id="xpBar" style="width:0%"></div>
|
||
</div>
|
||
<div class="xp-label">
|
||
<span id="xpVal">0 XP</span>
|
||
<span>10 XP = Level</span>
|
||
</div>
|
||
<div class="field" style="margin-top:12px;">
|
||
<label>Zenit (currency)</label>
|
||
<input type="number" id="zenit" value="0" min="0">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">⊕</span> Defenses</div>
|
||
<div class="def-row">
|
||
<div class="def-block">
|
||
<label>Initiative Mod</label>
|
||
<input type="number" id="initMod" value="0">
|
||
</div>
|
||
<div class="def-block">
|
||
<label>Defense</label>
|
||
<input type="number" id="defense" value="0">
|
||
</div>
|
||
<div class="def-block">
|
||
<label>Magic Defense</label>
|
||
<input type="number" id="magDef" value="0">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 2: Attributes + Status + HP/MP/IP -->
|
||
<div class="grid-3" style="margin-bottom:20px;">
|
||
|
||
<!-- Attributes -->
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">◈</span> Attributes</div>
|
||
<div class="attr-grid">
|
||
<div class="attr-block">
|
||
<div class="attr-name">Dexterity</div>
|
||
<div class="attr-inputs">
|
||
<div style="flex:1">
|
||
<label style="font-size:0.48rem;">Base</label>
|
||
<input type="number" id="dex-base" value="6" min="6" max="12" oninput="recalcStats()">
|
||
</div>
|
||
<div class="attr-sep">→</div>
|
||
<div style="flex:1">
|
||
<label style="font-size:0.48rem;">Current</label>
|
||
<input type="number" id="dex-cur" value="6" min="6" max="12">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="attr-block">
|
||
<div class="attr-name">Insight</div>
|
||
<div class="attr-inputs">
|
||
<div style="flex:1">
|
||
<label style="font-size:0.48rem;">Base</label>
|
||
<input type="number" id="ins-base" value="6" min="6" max="12" oninput="recalcStats()">
|
||
</div>
|
||
<div class="attr-sep">→</div>
|
||
<div style="flex:1">
|
||
<label style="font-size:0.48rem;">Current</label>
|
||
<input type="number" id="ins-cur" value="6" min="6" max="12">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="attr-block">
|
||
<div class="attr-name">Might</div>
|
||
<div class="attr-inputs">
|
||
<div style="flex:1">
|
||
<label style="font-size:0.48rem;">Base</label>
|
||
<input type="number" id="mig-base" value="6" min="6" max="12" oninput="recalcStats()">
|
||
</div>
|
||
<div class="attr-sep">→</div>
|
||
<div style="flex:1">
|
||
<label style="font-size:0.48rem;">Current</label>
|
||
<input type="number" id="mig-cur" value="6" min="6" max="12">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="attr-block">
|
||
<div class="attr-name">Willpower</div>
|
||
<div class="attr-inputs">
|
||
<div style="flex:1">
|
||
<label style="font-size:0.48rem;">Base</label>
|
||
<input type="number" id="wlp-base" value="6" min="6" max="12" oninput="recalcStats()">
|
||
</div>
|
||
<div class="attr-sep">→</div>
|
||
<div style="flex:1">
|
||
<label style="font-size:0.48rem;">Current</label>
|
||
<input type="number" id="wlp-cur" value="6" min="6" max="12">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Status Effects -->
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">⚠</span> Status Effects</div>
|
||
<div class="status-grid" id="statusGrid">
|
||
<!-- generated by JS -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- HP / MP / IP -->
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">♥</span> Hit, Mind & Inventory Points</div>
|
||
|
||
<div class="vital-block">
|
||
<div class="vital-label hp">HP</div>
|
||
<div style="flex:1">
|
||
<div class="vital-formula">MIG×5 + Level + Other</div>
|
||
<div class="vital-inputs">
|
||
<input type="number" id="hpMax" placeholder="Max" oninput="updateCrisis()">
|
||
<div class="vital-sep">/ </div>
|
||
<input type="number" id="hpCur" placeholder="Cur" oninput="updateCrisis()">
|
||
<div class="crisis-badge" id="crisisBadge" style="display:none">CRISIS</div>
|
||
</div>
|
||
</div>
|
||
<button class="add-btn" style="padding:4px 8px;" onclick="calcHP()"
|
||
title="Auto-calculate from Might">Calc</button>
|
||
</div>
|
||
|
||
<div class="vital-block">
|
||
<div class="vital-label mp">MP</div>
|
||
<div style="flex:1">
|
||
<div class="vital-formula">WLP×5 + Level + Other</div>
|
||
<div class="vital-inputs">
|
||
<input type="number" id="mpMax" placeholder="Max">
|
||
<div class="vital-sep">/</div>
|
||
<input type="number" id="mpCur" placeholder="Cur">
|
||
</div>
|
||
</div>
|
||
<button class="add-btn" style="padding:4px 8px;" onclick="calcMP()"
|
||
title="Auto-calculate from Willpower">Calc</button>
|
||
</div>
|
||
|
||
<div class="vital-block">
|
||
<div class="vital-label ip">IP</div>
|
||
<div style="flex:1">
|
||
<div class="vital-formula">6 + Other</div>
|
||
<div class="vital-inputs">
|
||
<input type="number" id="ipMax" value="6" placeholder="Max">
|
||
<div class="vital-sep">/</div>
|
||
<input type="number" id="ipCur" value="6" placeholder="Cur">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 3: Fabula Points + Bonds -->
|
||
<div class="grid-2" style="margin-bottom:20px;">
|
||
|
||
<!-- Fabula Points -->
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">✦</span> Fabula Points</div>
|
||
<div style="display:flex; align-items:center; gap:14px; flex-wrap:wrap;">
|
||
<div>
|
||
<label>Current FP</label>
|
||
<input type="number" id="fpCount" value="0" min="0" max="20"
|
||
style="width:70px; text-align:center; font-size:1.4rem; font-family:var(--font-mono);"
|
||
oninput="renderFP()">
|
||
</div>
|
||
<div class="fp-pips" id="fpPips"></div>
|
||
</div>
|
||
<div class="fp-rules">
|
||
<div class="fp-rule"><strong>+1 FP</strong> if you have none at start of session.</div>
|
||
<div class="fp-rule"><strong>+1 FP</strong> when a Villain makes an entrance.</div>
|
||
<div class="fp-rule"><strong>+1 FP</strong> when you fumble a Check.</div>
|
||
<div class="fp-rule"><strong>+2 FP</strong> if you surrender at zero HP.</div>
|
||
<div class="fp-rule" style="margin-top:6px;"><strong>Spend 1 FP</strong> to invoke a trait: reroll one or both
|
||
dice.</div>
|
||
<div class="fp-rule"><strong>Spend 1 FP</strong> to invoke a bond: add its strength to the result.</div>
|
||
<div class="fp-rule"><strong>Spend 1 FP</strong> to alter the story.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bonds -->
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">⊗</span> Bonds</div>
|
||
<div id="bondsContainer"></div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Row 4: Equipment + Backpack -->
|
||
<div class="grid-2" style="margin-bottom:20px;">
|
||
|
||
<!-- Equipment -->
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">⚔</span> Equipment</div>
|
||
<div class="martial-row" id="martialRow">
|
||
<div class="martial-item" onclick="toggleMartial(this)">
|
||
<div class="martial-box"></div><span>Martial Armor</span>
|
||
</div>
|
||
<div class="martial-item" onclick="toggleMartial(this)">
|
||
<div class="martial-box"></div><span>Martial Shields</span>
|
||
</div>
|
||
<div class="martial-item" onclick="toggleMartial(this)">
|
||
<div class="martial-box"></div><span>Martial Melee</span>
|
||
</div>
|
||
<div class="martial-item" onclick="toggleMartial(this)">
|
||
<div class="martial-box"></div><span>Martial Ranged</span>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top:14px;">
|
||
<div class="equip-row">
|
||
<div class="equip-slot">Accessory</div>
|
||
<div class="equip-fields">
|
||
<input type="text" id="acc-name" placeholder="Item name">
|
||
<input type="text" id="acc-desc" placeholder="Description / effect">
|
||
</div>
|
||
</div>
|
||
<div class="equip-row">
|
||
<div class="equip-slot">Armor</div>
|
||
<div class="equip-fields">
|
||
<input type="text" id="arm-name" placeholder="Item name">
|
||
<input type="text" id="arm-desc" placeholder="Defense bonus / effect">
|
||
</div>
|
||
</div>
|
||
<div class="equip-row">
|
||
<div class="equip-slot">Main Hand</div>
|
||
<div class="equip-fields">
|
||
<input type="text" id="mh-name" placeholder="Weapon name">
|
||
<input type="text" id="mh-desc" placeholder="Damage / effect">
|
||
</div>
|
||
</div>
|
||
<div class="equip-row">
|
||
<div class="equip-slot">Off-Hand</div>
|
||
<div class="equip-fields">
|
||
<input type="text" id="oh-name" placeholder="Weapon / shield">
|
||
<input type="text" id="oh-desc" placeholder="Damage / effect">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Backpack & Notes -->
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">◉</span> Backpack & Notes</div>
|
||
<textarea id="backpack" placeholder="Items, notes, lore…" style="min-height:200px;"></textarea>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════════════════
|
||
PAGE 2: CLASSES
|
||
══════════════════════════════════════════════════ -->
|
||
<div class="page" id="page-classes">
|
||
|
||
<div class="grid-2" style="margin-bottom:20px;">
|
||
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">✦</span> Primary Classes (up to 3 levels each)</div>
|
||
<div id="primaryClassContainer"></div>
|
||
<button class="add-btn" onclick="addPrimaryClass()">+ Add Primary Class</button>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">◎</span> Other Classes (max 3 non-mastered)</div>
|
||
<div id="otherClassContainer"></div>
|
||
<button class="add-btn" onclick="addOtherClass()">+ Add Class</button>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">★</span> Heroic Skills</div>
|
||
<textarea id="heroicSkills" placeholder="Record your heroic skill abilities here…"
|
||
style="min-height:100px;"></textarea>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════════════════
|
||
PAGE 3: ARCANA & SPELLS
|
||
══════════════════════════════════════════════════ -->
|
||
<div class="page" id="page-spells">
|
||
|
||
<div class="section" style="margin-bottom:20px;">
|
||
<div class="section-title"><span class="icon">✦</span> Arcana & Spells</div>
|
||
<table class="spells-table" id="spellsTable">
|
||
<thead>
|
||
<tr>
|
||
<th class="spell-name-col">Name / Notes</th>
|
||
<th class="spell-mp-col">MP Cost</th>
|
||
<th class="spell-targets-col">Targets</th>
|
||
<th class="spell-dur-col">Duration</th>
|
||
<th class="spell-del-col"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="spellsBody"></tbody>
|
||
</table>
|
||
<button class="add-btn" onclick="addSpell()" style="margin-top:10px;">+ Add Spell / Arcana</button>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">⊕</span> Rituals</div>
|
||
<div class="disciplines-row" id="disciplinesRow">
|
||
<div class="disc-item" onclick="toggleDisc(this)">
|
||
<div class="disc-box"></div><span>Arcanism</span>
|
||
</div>
|
||
<div class="disc-item" onclick="toggleDisc(this)">
|
||
<div class="disc-box"></div><span>Chimerism</span>
|
||
</div>
|
||
<div class="disc-item" onclick="toggleDisc(this)">
|
||
<div class="disc-box"></div><span>Elementalism</span>
|
||
</div>
|
||
<div class="disc-item" onclick="toggleDisc(this)">
|
||
<div class="disc-box"></div><span>Entropism</span>
|
||
</div>
|
||
<div class="disc-item" onclick="toggleDisc(this)">
|
||
<div class="disc-box"></div><span>Ritualism</span>
|
||
</div>
|
||
<div class="disc-item" onclick="toggleDisc(this)">
|
||
<div class="disc-box"></div><span>Spiritism</span>
|
||
</div>
|
||
</div>
|
||
<textarea id="ritualsNotes" placeholder="Record ritual details, components, and notes here…"
|
||
style="min-height:120px;"></textarea>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════════════════
|
||
PAGE 4: MANAGE
|
||
══════════════════════════════════════════════════ -->
|
||
<div class="page" id="page-manage">
|
||
<div class="manage-grid">
|
||
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">◈</span> Local Save</div>
|
||
<p class="manage-desc">Save your character sheet to your browser's local storage, or load a previously saved sheet.</p>
|
||
<div class="manage-btn-row">
|
||
<button class="btn-save btn-lg" onclick="saveSheet()">✦ Save to Browser</button>
|
||
<button class="btn-load btn-lg" onclick="loadSheet()">↑ Load from Browser</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-title"><span class="icon">⊕</span> JSON File</div>
|
||
<p class="manage-desc">Export your character to a JSON file for backup or sharing, or import from a previously exported file.</p>
|
||
<div class="manage-btn-row">
|
||
<button class="btn-save btn-export btn-lg" onclick="exportSheet()">↓ Export JSON</button>
|
||
<button class="btn-load btn-import btn-lg" onclick="importSheet()">↑ Import JSON</button>
|
||
<input type="file" id="importFileInput" accept=".json,application/json" style="display:none"
|
||
onchange="handleImportFile(this)">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section col-span-2">
|
||
<div class="section-title"><span class="icon">⎘</span> Share via URL</div>
|
||
<p class="manage-desc">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.</p>
|
||
<div class="manage-btn-row">
|
||
<button class="btn-save btn-export btn-lg" onclick="copyShareURL()">⎘ Copy URL</button>
|
||
<span class="save-status" id="copyStatus">Copied!</span>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<script src="fabula-ultima-sheet.js"></script>
|
||
</body>
|
||
|
||
</html> |