563 lines
21 KiB
HTML
563 lines
21 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" />
|
||
<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="hpCur" placeholder="Cur" oninput="updateCrisis()" />
|
||
<div class="vital-sep">/</div>
|
||
<input type="number" id="hpMax" placeholder="Max" 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="mpCur" placeholder="Cur" />
|
||
<div class="vital-sep">/</div>
|
||
<input type="number" id="mpMax" placeholder="Max" />
|
||
</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="ipCur" value="6" placeholder="Cur" />
|
||
<div class="vital-sep">/</div>
|
||
<input type="number" id="ipMax" value="6" placeholder="Max" />
|
||
</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>
|
||
</body>
|
||
|
||
</html> |