688 lines
24 KiB
HTML
688 lines
24 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="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>
|
||
</body>
|
||
</html>
|