chore: Format code with Prettier
This commit is contained in:
@@ -14,9 +14,9 @@
|
|||||||
--text-dim: #7a8a82;
|
--text-dim: #7a8a82;
|
||||||
--text-bright: #ede8dc;
|
--text-bright: #ede8dc;
|
||||||
--crisis: #e74c3c;
|
--crisis: #e74c3c;
|
||||||
--font-display: 'Cinzel', serif;
|
--font-display: "Cinzel", serif;
|
||||||
--font-body: 'Crimson Text', serif;
|
--font-body: "Crimson Text", serif;
|
||||||
--font-mono: 'Inconsolata', monospace;
|
--font-mono: "Inconsolata", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -32,8 +32,16 @@ body {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(ellipse at 20% 10%, rgba(78, 205, 196, 0.04) 0%, transparent 50%),
|
radial-gradient(
|
||||||
radial-gradient(ellipse at 80% 90%, rgba(201, 168, 76, 0.04) 0%, transparent 50%);
|
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 ─────────────────────────────────────── */
|
/* ── HEADER ─────────────────────────────────────── */
|
||||||
@@ -62,7 +70,7 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo::before {
|
.logo::before {
|
||||||
content: '✦';
|
content: "✦";
|
||||||
color: var(--gold);
|
color: var(--gold);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
@@ -146,7 +154,7 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section::before {
|
.section::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -204,7 +212,9 @@ select {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
padding: 8px 11px;
|
padding: 8px 11px;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.2s, box-shadow 0.2s;
|
transition:
|
||||||
|
border-color 0.2s,
|
||||||
|
box-shadow 0.2s;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,7 +512,7 @@ input[type="number"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fp-rule::before {
|
.fp-rule::before {
|
||||||
content: '✦';
|
content: "✦";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
font-size: 0.5rem;
|
font-size: 0.5rem;
|
||||||
@@ -1034,8 +1044,16 @@ input[type="number"] {
|
|||||||
|
|
||||||
[data-theme="light"] body {
|
[data-theme="light"] body {
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(ellipse at 20% 10%, rgba(26, 122, 118, 0.07) 0%, transparent 50%),
|
radial-gradient(
|
||||||
radial-gradient(ellipse at 80% 90%, rgba(122, 90, 24, 0.07) 0%, transparent 50%);
|
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 {
|
[data-theme="light"] header {
|
||||||
@@ -1075,7 +1093,6 @@ input[type="number"] {
|
|||||||
|
|
||||||
/* ── RESPONSIVE ──────────────────────────────────── */
|
/* ── RESPONSIVE ──────────────────────────────────── */
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
|
|
||||||
.grid-2,
|
.grid-2,
|
||||||
.grid-3 {
|
.grid-3 {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -1,100 +1,139 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en" data-theme="dark">
|
<html lang="en" data-theme="dark">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Fabula Ultima — Character Sheet</title>
|
<title>Fabula Ultima — Character Sheet</title>
|
||||||
<link
|
<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"
|
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">
|
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>
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<div class="logo">Fabula Ultima</div>
|
<div class="logo">Fabula Ultima</div>
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab active" onclick="switchTab('main')">Character</button>
|
<button class="tab active" onclick="switchTab('main')">
|
||||||
|
Character
|
||||||
|
</button>
|
||||||
<button class="tab" onclick="switchTab('classes')">Classes</button>
|
<button class="tab" onclick="switchTab('classes')">Classes</button>
|
||||||
<button class="tab" onclick="switchTab('spells')">Arcana & Spells</button>
|
<button class="tab" onclick="switchTab('spells')">
|
||||||
|
Arcana & Spells
|
||||||
|
</button>
|
||||||
<button class="tab" onclick="switchTab('manage')">Manage</button>
|
<button class="tab" onclick="switchTab('manage')">Manage</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<span class="save-status" id="saveStatus">Saved!</span>
|
<span class="save-status" id="saveStatus">Saved!</span>
|
||||||
<button class="btn-theme" id="themeToggle" onclick="toggleTheme()">☀ Light</button>
|
<button class="btn-theme" id="themeToggle" onclick="toggleTheme()">
|
||||||
|
☀ Light
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="urlBanner" class="url-banner" style="display: none">
|
<div id="urlBanner" class="url-banner" style="display: none">
|
||||||
<span class="url-banner-icon">⚑</span>
|
<span class="url-banner-icon">⚑</span>
|
||||||
Viewing a shared character — auto-save is disabled.
|
Viewing a shared character — auto-save is disabled. Use
|
||||||
Use <strong>Manage → Save to Browser</strong> to keep any changes.
|
<strong>Manage → Save to Browser</strong> to keep any changes.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ══════════════════════════════════════════════════
|
<!-- ══════════════════════════════════════════════════
|
||||||
PAGE 1: MAIN CHARACTER
|
PAGE 1: MAIN CHARACTER
|
||||||
══════════════════════════════════════════════════ -->
|
══════════════════════════════════════════════════ -->
|
||||||
<div class="page active" id="page-main">
|
<div class="page active" id="page-main">
|
||||||
|
|
||||||
<!-- Row 1: Identity + Level -->
|
<!-- Row 1: Identity + Level -->
|
||||||
<div class="grid-2" style="margin-bottom:20px;">
|
<div class="grid-2" style="margin-bottom: 20px">
|
||||||
|
|
||||||
<!-- Identity -->
|
<!-- Identity -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon">✦</span> Identity & Traits</div>
|
<div class="section-title">
|
||||||
|
<span class="icon">✦</span> Identity & Traits
|
||||||
|
</div>
|
||||||
<div class="field-row">
|
<div class="field-row">
|
||||||
<div class="field" style="flex: 2">
|
<div class="field" style="flex: 2">
|
||||||
<label>Name</label>
|
<label>Name</label>
|
||||||
<input type="text" id="charName" placeholder="Character name…">
|
<input type="text" id="charName" placeholder="Character name…" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Pronouns</label>
|
<label>Pronouns</label>
|
||||||
<input type="text" id="charPronouns" placeholder="they/them">
|
<input type="text" id="charPronouns" placeholder="they/them" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Identity</label>
|
<label>Identity</label>
|
||||||
<input type="text" id="charIdentity" placeholder="Who are you?">
|
<input type="text" id="charIdentity" placeholder="Who are you?" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field-row">
|
<div class="field-row">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Theme</label>
|
<label>Theme</label>
|
||||||
<input type="text" id="charTheme" placeholder="Your theme…">
|
<input type="text" id="charTheme" placeholder="Your theme…" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Origin</label>
|
<label>Origin</label>
|
||||||
<input type="text" id="charOrigin" placeholder="Where from?">
|
<input type="text" id="charOrigin" placeholder="Where from?" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Traits (comma-separated)</label>
|
<label>Traits (comma-separated)</label>
|
||||||
<textarea id="charTraits" placeholder="Brave, Reckless, Loyal to a fault…"
|
<textarea
|
||||||
style="min-height:55px;"></textarea>
|
id="charTraits"
|
||||||
|
placeholder="Brave, Reckless, Loyal to a fault…"
|
||||||
|
style="min-height: 55px"
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Level + XP + Defenses -->
|
<!-- Level + XP + Defenses -->
|
||||||
<div style="display:flex; flex-direction:column; gap:16px;">
|
<div style="display: flex; flex-direction: column; gap: 16px">
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon">⬡</span> Level & Experience</div>
|
<div class="section-title">
|
||||||
<div class="grid-2" style="gap:14px;">
|
<span class="icon">⬡</span> Level & Experience
|
||||||
|
</div>
|
||||||
|
<div class="grid-2" style="gap: 14px">
|
||||||
<div class="level-display">
|
<div class="level-display">
|
||||||
<span class="level-num" id="levelDisplay">1</span>
|
<span class="level-num" id="levelDisplay">1</span>
|
||||||
<span class="level-text">Character Level</span>
|
<span class="level-text">Character Level</span>
|
||||||
<button class="add-btn" style="margin-top:10px; width:100%; justify-content:center;"
|
<button
|
||||||
onclick="adjustLevel(1)">+ Level Up</button>
|
class="add-btn"
|
||||||
<button class="add-btn"
|
style="margin-top: 10px; width: 100%; justify-content: center"
|
||||||
style="margin-top:4px; width:100%; justify-content:center; border-color:var(--border-bright); color:var(--text-dim);"
|
onclick="adjustLevel(1)"
|
||||||
onclick="adjustLevel(-1)">− Level Down</button>
|
>
|
||||||
|
+ 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>
|
<div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Experience Points (XP)</label>
|
<label>Experience Points (XP)</label>
|
||||||
<input type="number" id="xpCurrent" value="0" min="0" oninput="updateXPBar()">
|
<input
|
||||||
|
type="number"
|
||||||
|
id="xpCurrent"
|
||||||
|
value="0"
|
||||||
|
min="0"
|
||||||
|
oninput="updateXPBar()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="xp-bar-wrap">
|
<div class="xp-bar-wrap">
|
||||||
<div class="xp-bar" id="xpBar" style="width: 0%"></div>
|
<div class="xp-bar" id="xpBar" style="width: 0%"></div>
|
||||||
@@ -103,53 +142,68 @@
|
|||||||
<span id="xpVal">0 XP</span>
|
<span id="xpVal">0 XP</span>
|
||||||
<span>10 XP = Level</span>
|
<span>10 XP = Level</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="field" style="margin-top:12px;">
|
<div class="field" style="margin-top: 12px">
|
||||||
<label>Zenit (currency)</label>
|
<label>Zenit (currency)</label>
|
||||||
<input type="number" id="zenit" value="0" min="0">
|
<input type="number" id="zenit" value="0" min="0" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon">⊕</span> Defenses</div>
|
<div class="section-title">
|
||||||
|
<span class="icon">⊕</span> Defenses
|
||||||
|
</div>
|
||||||
<div class="def-row">
|
<div class="def-row">
|
||||||
<div class="def-block">
|
<div class="def-block">
|
||||||
<label>Initiative Mod</label>
|
<label>Initiative Mod</label>
|
||||||
<input type="number" id="initMod" value="0">
|
<input type="number" id="initMod" value="0" />
|
||||||
</div>
|
</div>
|
||||||
<div class="def-block">
|
<div class="def-block">
|
||||||
<label>Defense</label>
|
<label>Defense</label>
|
||||||
<input type="number" id="defense" value="0">
|
<input type="number" id="defense" value="0" />
|
||||||
</div>
|
</div>
|
||||||
<div class="def-block">
|
<div class="def-block">
|
||||||
<label>Magic Defense</label>
|
<label>Magic Defense</label>
|
||||||
<input type="number" id="magDef" value="0">
|
<input type="number" id="magDef" value="0" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Row 2: Attributes + Status + HP/MP/IP -->
|
<!-- Row 2: Attributes + Status + HP/MP/IP -->
|
||||||
<div class="grid-3" style="margin-bottom:20px;">
|
<div class="grid-3" style="margin-bottom: 20px">
|
||||||
|
|
||||||
<!-- Attributes -->
|
<!-- Attributes -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon">◈</span> Attributes</div>
|
<div class="section-title">
|
||||||
|
<span class="icon">◈</span> Attributes
|
||||||
|
</div>
|
||||||
<div class="attr-grid">
|
<div class="attr-grid">
|
||||||
<div class="attr-block">
|
<div class="attr-block">
|
||||||
<div class="attr-name">Dexterity</div>
|
<div class="attr-name">Dexterity</div>
|
||||||
<div class="attr-inputs">
|
<div class="attr-inputs">
|
||||||
<div style="flex: 1">
|
<div style="flex: 1">
|
||||||
<label style="font-size:0.48rem;">Base</label>
|
<label style="font-size: 0.48rem">Base</label>
|
||||||
<input type="number" id="dex-base" value="6" min="6" max="12" oninput="recalcStats()">
|
<input
|
||||||
|
type="number"
|
||||||
|
id="dex-base"
|
||||||
|
value="6"
|
||||||
|
min="6"
|
||||||
|
max="12"
|
||||||
|
oninput="recalcStats()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="attr-sep">→</div>
|
<div class="attr-sep">→</div>
|
||||||
<div style="flex: 1">
|
<div style="flex: 1">
|
||||||
<label style="font-size:0.48rem;">Current</label>
|
<label style="font-size: 0.48rem">Current</label>
|
||||||
<input type="number" id="dex-cur" value="6" min="6" max="12">
|
<input
|
||||||
|
type="number"
|
||||||
|
id="dex-cur"
|
||||||
|
value="6"
|
||||||
|
min="6"
|
||||||
|
max="12"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,13 +211,26 @@
|
|||||||
<div class="attr-name">Insight</div>
|
<div class="attr-name">Insight</div>
|
||||||
<div class="attr-inputs">
|
<div class="attr-inputs">
|
||||||
<div style="flex: 1">
|
<div style="flex: 1">
|
||||||
<label style="font-size:0.48rem;">Base</label>
|
<label style="font-size: 0.48rem">Base</label>
|
||||||
<input type="number" id="ins-base" value="6" min="6" max="12" oninput="recalcStats()">
|
<input
|
||||||
|
type="number"
|
||||||
|
id="ins-base"
|
||||||
|
value="6"
|
||||||
|
min="6"
|
||||||
|
max="12"
|
||||||
|
oninput="recalcStats()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="attr-sep">→</div>
|
<div class="attr-sep">→</div>
|
||||||
<div style="flex: 1">
|
<div style="flex: 1">
|
||||||
<label style="font-size:0.48rem;">Current</label>
|
<label style="font-size: 0.48rem">Current</label>
|
||||||
<input type="number" id="ins-cur" value="6" min="6" max="12">
|
<input
|
||||||
|
type="number"
|
||||||
|
id="ins-cur"
|
||||||
|
value="6"
|
||||||
|
min="6"
|
||||||
|
max="12"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,13 +238,26 @@
|
|||||||
<div class="attr-name">Might</div>
|
<div class="attr-name">Might</div>
|
||||||
<div class="attr-inputs">
|
<div class="attr-inputs">
|
||||||
<div style="flex: 1">
|
<div style="flex: 1">
|
||||||
<label style="font-size:0.48rem;">Base</label>
|
<label style="font-size: 0.48rem">Base</label>
|
||||||
<input type="number" id="mig-base" value="6" min="6" max="12" oninput="recalcStats()">
|
<input
|
||||||
|
type="number"
|
||||||
|
id="mig-base"
|
||||||
|
value="6"
|
||||||
|
min="6"
|
||||||
|
max="12"
|
||||||
|
oninput="recalcStats()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="attr-sep">→</div>
|
<div class="attr-sep">→</div>
|
||||||
<div style="flex: 1">
|
<div style="flex: 1">
|
||||||
<label style="font-size:0.48rem;">Current</label>
|
<label style="font-size: 0.48rem">Current</label>
|
||||||
<input type="number" id="mig-cur" value="6" min="6" max="12">
|
<input
|
||||||
|
type="number"
|
||||||
|
id="mig-cur"
|
||||||
|
value="6"
|
||||||
|
min="6"
|
||||||
|
max="12"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,13 +265,26 @@
|
|||||||
<div class="attr-name">Willpower</div>
|
<div class="attr-name">Willpower</div>
|
||||||
<div class="attr-inputs">
|
<div class="attr-inputs">
|
||||||
<div style="flex: 1">
|
<div style="flex: 1">
|
||||||
<label style="font-size:0.48rem;">Base</label>
|
<label style="font-size: 0.48rem">Base</label>
|
||||||
<input type="number" id="wlp-base" value="6" min="6" max="12" oninput="recalcStats()">
|
<input
|
||||||
|
type="number"
|
||||||
|
id="wlp-base"
|
||||||
|
value="6"
|
||||||
|
min="6"
|
||||||
|
max="12"
|
||||||
|
oninput="recalcStats()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="attr-sep">→</div>
|
<div class="attr-sep">→</div>
|
||||||
<div style="flex: 1">
|
<div style="flex: 1">
|
||||||
<label style="font-size:0.48rem;">Current</label>
|
<label style="font-size: 0.48rem">Current</label>
|
||||||
<input type="number" id="wlp-cur" value="6" min="6" max="12">
|
<input
|
||||||
|
type="number"
|
||||||
|
id="wlp-cur"
|
||||||
|
value="6"
|
||||||
|
min="6"
|
||||||
|
max="12"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -200,7 +293,9 @@
|
|||||||
|
|
||||||
<!-- Status Effects -->
|
<!-- Status Effects -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon">⚠</span> Status Effects</div>
|
<div class="section-title">
|
||||||
|
<span class="icon">⚠</span> Status Effects
|
||||||
|
</div>
|
||||||
<div class="status-grid" id="statusGrid">
|
<div class="status-grid" id="statusGrid">
|
||||||
<!-- generated by JS -->
|
<!-- generated by JS -->
|
||||||
</div>
|
</div>
|
||||||
@@ -208,21 +303,45 @@
|
|||||||
|
|
||||||
<!-- HP / MP / IP -->
|
<!-- HP / MP / IP -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon">♥</span> Hit, Mind & Inventory Points</div>
|
<div class="section-title">
|
||||||
|
<span class="icon">♥</span> Hit, Mind & Inventory Points
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="vital-block">
|
<div class="vital-block">
|
||||||
<div class="vital-label hp">HP</div>
|
<div class="vital-label hp">HP</div>
|
||||||
<div style="flex: 1">
|
<div style="flex: 1">
|
||||||
<div class="vital-formula">MIG×5 + Level + Other</div>
|
<div class="vital-formula">MIG×5 + Level + Other</div>
|
||||||
<div class="vital-inputs">
|
<div class="vital-inputs">
|
||||||
<input type="number" id="hpMax" placeholder="Max" oninput="updateCrisis()">
|
<input
|
||||||
|
type="number"
|
||||||
|
id="hpMax"
|
||||||
|
placeholder="Max"
|
||||||
|
oninput="updateCrisis()"
|
||||||
|
/>
|
||||||
<div class="vital-sep">/</div>
|
<div class="vital-sep">/</div>
|
||||||
<input type="number" id="hpCur" placeholder="Cur" oninput="updateCrisis()">
|
<input
|
||||||
<div class="crisis-badge" id="crisisBadge" style="display:none">CRISIS</div>
|
type="number"
|
||||||
|
id="hpCur"
|
||||||
|
placeholder="Cur"
|
||||||
|
oninput="updateCrisis()"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="crisis-badge"
|
||||||
|
id="crisisBadge"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
CRISIS
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="add-btn" style="padding:4px 8px;" onclick="calcHP()"
|
</div>
|
||||||
title="Auto-calculate from Might">Calc</button>
|
<button
|
||||||
|
class="add-btn"
|
||||||
|
style="padding: 4px 8px"
|
||||||
|
onclick="calcHP()"
|
||||||
|
title="Auto-calculate from Might"
|
||||||
|
>
|
||||||
|
Calc
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="vital-block">
|
<div class="vital-block">
|
||||||
@@ -230,13 +349,19 @@
|
|||||||
<div style="flex: 1">
|
<div style="flex: 1">
|
||||||
<div class="vital-formula">WLP×5 + Level + Other</div>
|
<div class="vital-formula">WLP×5 + Level + Other</div>
|
||||||
<div class="vital-inputs">
|
<div class="vital-inputs">
|
||||||
<input type="number" id="mpMax" placeholder="Max">
|
<input type="number" id="mpMax" placeholder="Max" />
|
||||||
<div class="vital-sep">/</div>
|
<div class="vital-sep">/</div>
|
||||||
<input type="number" id="mpCur" placeholder="Cur">
|
<input type="number" id="mpCur" placeholder="Cur" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="add-btn" style="padding:4px 8px;" onclick="calcMP()"
|
<button
|
||||||
title="Auto-calculate from Willpower">Calc</button>
|
class="add-btn"
|
||||||
|
style="padding: 4px 8px"
|
||||||
|
onclick="calcMP()"
|
||||||
|
title="Auto-calculate from Willpower"
|
||||||
|
>
|
||||||
|
Calc
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="vital-block">
|
<div class="vital-block">
|
||||||
@@ -244,9 +369,9 @@
|
|||||||
<div style="flex: 1">
|
<div style="flex: 1">
|
||||||
<div class="vital-formula">6 + Other</div>
|
<div class="vital-formula">6 + Other</div>
|
||||||
<div class="vital-inputs">
|
<div class="vital-inputs">
|
||||||
<input type="number" id="ipMax" value="6" placeholder="Max">
|
<input type="number" id="ipMax" value="6" placeholder="Max" />
|
||||||
<div class="vital-sep">/</div>
|
<div class="vital-sep">/</div>
|
||||||
<input type="number" id="ipCur" value="6" placeholder="Cur">
|
<input type="number" id="ipCur" value="6" placeholder="Cur" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -254,29 +379,63 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Row 3: Fabula Points + Bonds -->
|
<!-- Row 3: Fabula Points + Bonds -->
|
||||||
<div class="grid-2" style="margin-bottom:20px;">
|
<div class="grid-2" style="margin-bottom: 20px">
|
||||||
|
|
||||||
<!-- Fabula Points -->
|
<!-- Fabula Points -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon">✦</span> Fabula Points</div>
|
<div class="section-title">
|
||||||
<div style="display:flex; align-items:center; gap:14px; flex-wrap:wrap;">
|
<span class="icon">✦</span> Fabula Points
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label>Current FP</label>
|
<label>Current FP</label>
|
||||||
<input type="number" id="fpCount" value="0" min="0" max="20"
|
<input
|
||||||
style="width:70px; text-align:center; font-size:1.4rem; font-family:var(--font-mono);"
|
type="number"
|
||||||
oninput="renderFP()">
|
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>
|
||||||
<div class="fp-pips" id="fpPips"></div>
|
<div class="fp-pips" id="fpPips"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fp-rules">
|
<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">
|
||||||
<div class="fp-rule"><strong>+1 FP</strong> when a Villain makes an entrance.</div>
|
<strong>+1 FP</strong> if you have none at start of session.
|
||||||
<div class="fp-rule"><strong>+1 FP</strong> when you fumble a Check.</div>
|
</div>
|
||||||
<div class="fp-rule"><strong>+2 FP</strong> if you surrender at zero HP.</div>
|
<div class="fp-rule">
|
||||||
<div class="fp-rule" style="margin-top:6px;"><strong>Spend 1 FP</strong> to invoke a trait: reroll one or both
|
<strong>+1 FP</strong> when a Villain makes an entrance.
|
||||||
dice.</div>
|
</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">
|
||||||
<div class="fp-rule"><strong>Spend 1 FP</strong> to alter the story.</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -285,56 +444,66 @@
|
|||||||
<div class="section-title"><span class="icon">⊗</span> Bonds</div>
|
<div class="section-title"><span class="icon">⊗</span> Bonds</div>
|
||||||
<div id="bondsContainer"></div>
|
<div id="bondsContainer"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Row 4: Equipment + Backpack -->
|
<!-- Row 4: Equipment + Backpack -->
|
||||||
<div class="grid-2" style="margin-bottom:20px;">
|
<div class="grid-2" style="margin-bottom: 20px">
|
||||||
|
|
||||||
<!-- Equipment -->
|
<!-- Equipment -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon">⚔</span> Equipment</div>
|
<div class="section-title"><span class="icon">⚔</span> Equipment</div>
|
||||||
<div class="martial-row" id="martialRow">
|
<div class="martial-row" id="martialRow">
|
||||||
<div class="martial-item" onclick="toggleMartial(this)">
|
<div class="martial-item" onclick="toggleMartial(this)">
|
||||||
<div class="martial-box"></div><span>Martial Armor</span>
|
<div class="martial-box"></div>
|
||||||
|
<span>Martial Armor</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="martial-item" onclick="toggleMartial(this)">
|
<div class="martial-item" onclick="toggleMartial(this)">
|
||||||
<div class="martial-box"></div><span>Martial Shields</span>
|
<div class="martial-box"></div>
|
||||||
|
<span>Martial Shields</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="martial-item" onclick="toggleMartial(this)">
|
<div class="martial-item" onclick="toggleMartial(this)">
|
||||||
<div class="martial-box"></div><span>Martial Melee</span>
|
<div class="martial-box"></div>
|
||||||
|
<span>Martial Melee</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="martial-item" onclick="toggleMartial(this)">
|
<div class="martial-item" onclick="toggleMartial(this)">
|
||||||
<div class="martial-box"></div><span>Martial Ranged</span>
|
<div class="martial-box"></div>
|
||||||
|
<span>Martial Ranged</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:14px;">
|
<div style="margin-top: 14px">
|
||||||
<div class="equip-row">
|
<div class="equip-row">
|
||||||
<div class="equip-slot">Accessory</div>
|
<div class="equip-slot">Accessory</div>
|
||||||
<div class="equip-fields">
|
<div class="equip-fields">
|
||||||
<input type="text" id="acc-name" placeholder="Item name">
|
<input type="text" id="acc-name" placeholder="Item name" />
|
||||||
<input type="text" id="acc-desc" placeholder="Description / effect">
|
<input
|
||||||
|
type="text"
|
||||||
|
id="acc-desc"
|
||||||
|
placeholder="Description / effect"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="equip-row">
|
<div class="equip-row">
|
||||||
<div class="equip-slot">Armor</div>
|
<div class="equip-slot">Armor</div>
|
||||||
<div class="equip-fields">
|
<div class="equip-fields">
|
||||||
<input type="text" id="arm-name" placeholder="Item name">
|
<input type="text" id="arm-name" placeholder="Item name" />
|
||||||
<input type="text" id="arm-desc" placeholder="Defense bonus / effect">
|
<input
|
||||||
|
type="text"
|
||||||
|
id="arm-desc"
|
||||||
|
placeholder="Defense bonus / effect"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="equip-row">
|
<div class="equip-row">
|
||||||
<div class="equip-slot">Main Hand</div>
|
<div class="equip-slot">Main Hand</div>
|
||||||
<div class="equip-fields">
|
<div class="equip-fields">
|
||||||
<input type="text" id="mh-name" placeholder="Weapon name">
|
<input type="text" id="mh-name" placeholder="Weapon name" />
|
||||||
<input type="text" id="mh-desc" placeholder="Damage / effect">
|
<input type="text" id="mh-desc" placeholder="Damage / effect" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="equip-row">
|
<div class="equip-row">
|
||||||
<div class="equip-slot">Off-Hand</div>
|
<div class="equip-slot">Off-Hand</div>
|
||||||
<div class="equip-fields">
|
<div class="equip-fields">
|
||||||
<input type="text" id="oh-name" placeholder="Weapon / shield">
|
<input type="text" id="oh-name" placeholder="Weapon / shield" />
|
||||||
<input type="text" id="oh-desc" placeholder="Damage / effect">
|
<input type="text" id="oh-desc" placeholder="Damage / effect" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -342,50 +511,62 @@
|
|||||||
|
|
||||||
<!-- Backpack & Notes -->
|
<!-- Backpack & Notes -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon">◉</span> Backpack & Notes</div>
|
<div class="section-title">
|
||||||
<textarea id="backpack" placeholder="Items, notes, lore…" style="min-height:200px;"></textarea>
|
<span class="icon">◉</span> Backpack & Notes
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
id="backpack"
|
||||||
|
placeholder="Items, notes, lore…"
|
||||||
|
style="min-height: 200px"
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ══════════════════════════════════════════════════
|
<!-- ══════════════════════════════════════════════════
|
||||||
PAGE 2: CLASSES
|
PAGE 2: CLASSES
|
||||||
══════════════════════════════════════════════════ -->
|
══════════════════════════════════════════════════ -->
|
||||||
<div class="page" id="page-classes">
|
<div class="page" id="page-classes">
|
||||||
|
<div class="grid-2" style="margin-bottom: 20px">
|
||||||
<div class="grid-2" style="margin-bottom:20px;">
|
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon">✦</span> Primary Classes (up to 3 levels each)</div>
|
<div class="section-title">
|
||||||
|
<span class="icon">✦</span> Primary Classes (up to 3 levels each)
|
||||||
|
</div>
|
||||||
<div id="primaryClassContainer"></div>
|
<div id="primaryClassContainer"></div>
|
||||||
<button class="add-btn" onclick="addPrimaryClass()">+ Add Primary Class</button>
|
<button class="add-btn" onclick="addPrimaryClass()">
|
||||||
|
+ Add Primary Class
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon">◎</span> Other Classes (max 3 non-mastered)</div>
|
<div class="section-title">
|
||||||
|
<span class="icon">◎</span> Other Classes (max 3 non-mastered)
|
||||||
|
</div>
|
||||||
<div id="otherClassContainer"></div>
|
<div id="otherClassContainer"></div>
|
||||||
<button class="add-btn" onclick="addOtherClass()">+ Add Class</button>
|
<button class="add-btn" onclick="addOtherClass()">+ Add Class</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon">★</span> Heroic Skills</div>
|
<div class="section-title">
|
||||||
<textarea id="heroicSkills" placeholder="Record your heroic skill abilities here…"
|
<span class="icon">★</span> Heroic Skills
|
||||||
style="min-height:100px;"></textarea>
|
</div>
|
||||||
|
<textarea
|
||||||
|
id="heroicSkills"
|
||||||
|
placeholder="Record your heroic skill abilities here…"
|
||||||
|
style="min-height: 100px"
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ══════════════════════════════════════════════════
|
<!-- ══════════════════════════════════════════════════
|
||||||
PAGE 3: ARCANA & SPELLS
|
PAGE 3: ARCANA & SPELLS
|
||||||
══════════════════════════════════════════════════ -->
|
══════════════════════════════════════════════════ -->
|
||||||
<div class="page" id="page-spells">
|
<div class="page" id="page-spells">
|
||||||
|
<div class="section" style="margin-bottom: 20px">
|
||||||
<div class="section" style="margin-bottom:20px;">
|
<div class="section-title">
|
||||||
<div class="section-title"><span class="icon">✦</span> Arcana & Spells</div>
|
<span class="icon">✦</span> Arcana & Spells
|
||||||
|
</div>
|
||||||
<table class="spells-table" id="spellsTable">
|
<table class="spells-table" id="spellsTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -398,35 +579,45 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="spellsBody"></tbody>
|
<tbody id="spellsBody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
<button class="add-btn" onclick="addSpell()" style="margin-top:10px;">+ Add Spell / Arcana</button>
|
<button class="add-btn" onclick="addSpell()" style="margin-top: 10px">
|
||||||
|
+ Add Spell / Arcana
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon">⊕</span> Rituals</div>
|
<div class="section-title"><span class="icon">⊕</span> Rituals</div>
|
||||||
<div class="disciplines-row" id="disciplinesRow">
|
<div class="disciplines-row" id="disciplinesRow">
|
||||||
<div class="disc-item" onclick="toggleDisc(this)">
|
<div class="disc-item" onclick="toggleDisc(this)">
|
||||||
<div class="disc-box"></div><span>Arcanism</span>
|
<div class="disc-box"></div>
|
||||||
|
<span>Arcanism</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="disc-item" onclick="toggleDisc(this)">
|
<div class="disc-item" onclick="toggleDisc(this)">
|
||||||
<div class="disc-box"></div><span>Chimerism</span>
|
<div class="disc-box"></div>
|
||||||
|
<span>Chimerism</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="disc-item" onclick="toggleDisc(this)">
|
<div class="disc-item" onclick="toggleDisc(this)">
|
||||||
<div class="disc-box"></div><span>Elementalism</span>
|
<div class="disc-box"></div>
|
||||||
|
<span>Elementalism</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="disc-item" onclick="toggleDisc(this)">
|
<div class="disc-item" onclick="toggleDisc(this)">
|
||||||
<div class="disc-box"></div><span>Entropism</span>
|
<div class="disc-box"></div>
|
||||||
|
<span>Entropism</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="disc-item" onclick="toggleDisc(this)">
|
<div class="disc-item" onclick="toggleDisc(this)">
|
||||||
<div class="disc-box"></div><span>Ritualism</span>
|
<div class="disc-box"></div>
|
||||||
|
<span>Ritualism</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="disc-item" onclick="toggleDisc(this)">
|
<div class="disc-item" onclick="toggleDisc(this)">
|
||||||
<div class="disc-box"></div><span>Spiritism</span>
|
<div class="disc-box"></div>
|
||||||
|
<span>Spiritism</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea id="ritualsNotes" placeholder="Record ritual details, components, and notes here…"
|
<textarea
|
||||||
style="min-height:120px;"></textarea>
|
id="ritualsNotes"
|
||||||
|
placeholder="Record ritual details, components, and notes here…"
|
||||||
|
style="min-height: 120px"
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ══════════════════════════════════════════════════
|
<!-- ══════════════════════════════════════════════════
|
||||||
@@ -434,40 +625,66 @@
|
|||||||
══════════════════════════════════════════════════ -->
|
══════════════════════════════════════════════════ -->
|
||||||
<div class="page" id="page-manage">
|
<div class="page" id="page-manage">
|
||||||
<div class="manage-grid">
|
<div class="manage-grid">
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon">◈</span> Local Save</div>
|
<div class="section-title">
|
||||||
<p class="manage-desc">Save your character sheet to your browser's local storage, or load a previously saved sheet.</p>
|
<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">
|
<div class="manage-btn-row">
|
||||||
<button class="btn-save btn-lg" onclick="saveSheet()">✦ Save to Browser</button>
|
<button class="btn-save btn-lg" onclick="saveSheet()">
|
||||||
<button class="btn-load btn-lg" onclick="loadSheet()">↑ Load from Browser</button>
|
✦ Save to Browser
|
||||||
|
</button>
|
||||||
|
<button class="btn-load btn-lg" onclick="loadSheet()">
|
||||||
|
↑ Load from Browser
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title"><span class="icon">⊕</span> JSON File</div>
|
<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>
|
<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">
|
<div class="manage-btn-row">
|
||||||
<button class="btn-save btn-export btn-lg" onclick="exportSheet()">↓ Export JSON</button>
|
<button class="btn-save btn-export btn-lg" onclick="exportSheet()">
|
||||||
<button class="btn-load btn-import btn-lg" onclick="importSheet()">↑ Import JSON</button>
|
↓ Export JSON
|
||||||
<input type="file" id="importFileInput" accept=".json,application/json" style="display:none"
|
</button>
|
||||||
onchange="handleImportFile(this)">
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="section col-span-2">
|
<div class="section col-span-2">
|
||||||
<div class="section-title"><span class="icon">⎘</span> Share via URL</div>
|
<div class="section-title">
|
||||||
<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>
|
<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">
|
<div class="manage-btn-row">
|
||||||
<button class="btn-save btn-export btn-lg" onclick="copyShareURL()">⎘ Copy URL</button>
|
<button class="btn-save btn-export btn-lg" onclick="copyShareURL()">
|
||||||
|
⎘ Copy URL
|
||||||
|
</button>
|
||||||
<span class="save-status" id="copyStatus">Copied!</span>
|
<span class="save-status" id="copyStatus">Copied!</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="fabula-ultima-sheet.js"></script>
|
<script src="fabula-ultima-sheet.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -6,30 +6,45 @@ let primaryClasses = [];
|
|||||||
let otherClasses = [];
|
let otherClasses = [];
|
||||||
let spells = [];
|
let spells = [];
|
||||||
let bonds = [
|
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 STATUSES = ["Slow", "Enraged", "Dazed", "Weak", "Poisoned", "Shaken"];
|
||||||
const FEELINGS = ['Admiration', 'Inferiority', 'Loyalty', 'Mistrust', 'Affection', 'Hatred'];
|
const FEELINGS = [
|
||||||
|
"Admiration",
|
||||||
|
"Inferiority",
|
||||||
|
"Loyalty",
|
||||||
|
"Mistrust",
|
||||||
|
"Affection",
|
||||||
|
"Hatred",
|
||||||
|
];
|
||||||
|
|
||||||
// ── TABS ───────────────────────────────────────────
|
// ── TABS ───────────────────────────────────────────
|
||||||
function switchTab(tab) {
|
function switchTab(tab) {
|
||||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
document
|
||||||
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
.querySelectorAll(".tab")
|
||||||
event.currentTarget.classList.add('active');
|
.forEach((t) => t.classList.remove("active"));
|
||||||
document.getElementById('page-' + tab).classList.add('active');
|
document
|
||||||
|
.querySelectorAll(".page")
|
||||||
|
.forEach((p) => p.classList.remove("active"));
|
||||||
|
event.currentTarget.classList.add("active");
|
||||||
|
document.getElementById("page-" + tab).classList.add("active");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── INIT ───────────────────────────────────────────
|
// ── INIT ───────────────────────────────────────────
|
||||||
async function init() {
|
async function init() {
|
||||||
const savedTheme = localStorage.getItem('fabulaUltimaTheme')
|
const savedTheme =
|
||||||
|| (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
|
localStorage.getItem("fabulaUltimaTheme") ||
|
||||||
document.getElementById('themeToggle').textContent = savedTheme === 'light' ? '☾ Dark' : '☀ Light';
|
(window.matchMedia("(prefers-color-scheme: light)").matches
|
||||||
|
? "light"
|
||||||
|
: "dark");
|
||||||
|
document.getElementById("themeToggle").textContent =
|
||||||
|
savedTheme === "light" ? "☾ Dark" : "☀ Light";
|
||||||
renderStatuses();
|
renderStatuses();
|
||||||
renderFP();
|
renderFP();
|
||||||
renderBonds();
|
renderBonds();
|
||||||
@@ -37,26 +52,27 @@ async function init() {
|
|||||||
renderOtherClasses();
|
renderOtherClasses();
|
||||||
renderSpells();
|
renderSpells();
|
||||||
updateXPBar();
|
updateXPBar();
|
||||||
if (!await tryLoadFromURL()) tryAutoLoad();
|
if (!(await tryLoadFromURL())) tryAutoLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── URL STATE ──────────────────────────────────────
|
// ── URL STATE ──────────────────────────────────────
|
||||||
async function compressToBase64(str) {
|
async function compressToBase64(str) {
|
||||||
const stream = new CompressionStream('deflate-raw');
|
const stream = new CompressionStream("deflate-raw");
|
||||||
const writer = stream.writable.getWriter();
|
const writer = stream.writable.getWriter();
|
||||||
writer.write(new TextEncoder().encode(str));
|
writer.write(new TextEncoder().encode(str));
|
||||||
writer.close();
|
writer.close();
|
||||||
const buf = await new Response(stream.readable).arrayBuffer();
|
const buf = await new Response(stream.readable).arrayBuffer();
|
||||||
const bytes = new Uint8Array(buf);
|
const bytes = new Uint8Array(buf);
|
||||||
let binary = '';
|
let binary = "";
|
||||||
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
for (let i = 0; i < bytes.length; i++)
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
return btoa(binary);
|
return btoa(binary);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function decompressFromBase64(b64) {
|
async function decompressFromBase64(b64) {
|
||||||
try {
|
try {
|
||||||
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
||||||
const stream = new DecompressionStream('deflate-raw');
|
const stream = new DecompressionStream("deflate-raw");
|
||||||
const writer = stream.writable.getWriter();
|
const writer = stream.writable.getWriter();
|
||||||
writer.write(bytes);
|
writer.write(bytes);
|
||||||
writer.close();
|
writer.close();
|
||||||
@@ -69,29 +85,36 @@ async function decompressFromBase64(b64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function tryLoadFromURL() {
|
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;
|
if (!encoded) return false;
|
||||||
try {
|
try {
|
||||||
const json = await decompressFromBase64(encoded);
|
const json = await decompressFromBase64(encoded);
|
||||||
applyData(JSON.parse(json));
|
applyData(JSON.parse(json));
|
||||||
urlMode = true;
|
urlMode = true;
|
||||||
document.getElementById('urlBanner').style.display = 'block';
|
document.getElementById("urlBanner").style.display = "block";
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Could not load state from URL:', e);
|
console.warn("Could not load state from URL:", e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── STATUS EFFECTS ─────────────────────────────────
|
// ── STATUS EFFECTS ─────────────────────────────────
|
||||||
function renderStatuses() {
|
function renderStatuses() {
|
||||||
const g = document.getElementById('statusGrid');
|
const g = document.getElementById("statusGrid");
|
||||||
g.innerHTML = '';
|
g.innerHTML = "";
|
||||||
STATUSES.forEach(s => {
|
STATUSES.forEach((s) => {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement("div");
|
||||||
el.className = 'status-item';
|
el.className = "status-item";
|
||||||
el.dataset.status = s;
|
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 = `<div class="status-check"></div><div class="status-label">${s}</div>`;
|
el.innerHTML = `<div class="status-check"></div><div class="status-label">${s}</div>`;
|
||||||
g.appendChild(el);
|
g.appendChild(el);
|
||||||
});
|
});
|
||||||
@@ -99,16 +122,16 @@ function renderStatuses() {
|
|||||||
|
|
||||||
// ── FABULA POINTS ──────────────────────────────────
|
// ── FABULA POINTS ──────────────────────────────────
|
||||||
function renderFP() {
|
function renderFP() {
|
||||||
fpCount = parseInt(document.getElementById('fpCount').value) || 0;
|
fpCount = parseInt(document.getElementById("fpCount").value) || 0;
|
||||||
const pips = document.getElementById('fpPips');
|
const pips = document.getElementById("fpPips");
|
||||||
const total = Math.max(10, fpCount);
|
const total = Math.max(10, fpCount);
|
||||||
pips.innerHTML = '';
|
pips.innerHTML = "";
|
||||||
for (let i = 0; i < total; i++) {
|
for (let i = 0; i < total; i++) {
|
||||||
const pip = document.createElement('div');
|
const pip = document.createElement("div");
|
||||||
pip.className = 'fp-pip' + (i < fpCount ? ' filled' : '');
|
pip.className = "fp-pip" + (i < fpCount ? " filled" : "");
|
||||||
pip.onclick = () => {
|
pip.onclick = () => {
|
||||||
const newVal = i < fpCount ? i : i + 1;
|
const newVal = i < fpCount ? i : i + 1;
|
||||||
document.getElementById('fpCount').value = newVal;
|
document.getElementById("fpCount").value = newVal;
|
||||||
renderFP();
|
renderFP();
|
||||||
};
|
};
|
||||||
pips.appendChild(pip);
|
pips.appendChild(pip);
|
||||||
@@ -117,22 +140,24 @@ function renderFP() {
|
|||||||
|
|
||||||
// ── BONDS ──────────────────────────────────────────
|
// ── BONDS ──────────────────────────────────────────
|
||||||
function renderBonds() {
|
function renderBonds() {
|
||||||
const container = document.getElementById('bondsContainer');
|
const container = document.getElementById("bondsContainer");
|
||||||
container.innerHTML = '';
|
container.innerHTML = "";
|
||||||
bonds.forEach((bond, idx) => {
|
bonds.forEach((bond, idx) => {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement("div");
|
||||||
el.className = 'bond-block';
|
el.className = "bond-block";
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="bond-header">
|
<div class="bond-header">
|
||||||
<div class="bond-num">${idx + 1}</div>
|
<div class="bond-num">${idx + 1}</div>
|
||||||
<input type="text" placeholder="Bond target name…" value="${bond.name}" oninput="bonds[${idx}].name=this.value">
|
<input type="text" placeholder="Bond target name…" value="${bond.name}" oninput="bonds[${idx}].name=this.value">
|
||||||
</div>
|
</div>
|
||||||
<div class="bond-feelings">
|
<div class="bond-feelings">
|
||||||
${FEELINGS.map(f => `
|
${FEELINGS.map(
|
||||||
<div class="bond-feeling${bond.feelings.includes(f) ? ' active' : ''}" onclick="toggleFeeling(${idx},'${f}',this)">
|
(f) => `
|
||||||
<div class="bond-feeling-box">${bond.feelings.includes(f) ? '✓' : ''}</div>
|
<div class="bond-feeling${bond.feelings.includes(f) ? " active" : ""}" onclick="toggleFeeling(${idx},'${f}',this)">
|
||||||
|
<div class="bond-feeling-box">${bond.feelings.includes(f) ? "✓" : ""}</div>
|
||||||
<span>${f}</span>
|
<span>${f}</span>
|
||||||
</div>`).join('')}
|
</div>`,
|
||||||
|
).join("")}
|
||||||
</div>`;
|
</div>`;
|
||||||
container.appendChild(el);
|
container.appendChild(el);
|
||||||
});
|
});
|
||||||
@@ -141,71 +166,88 @@ function renderBonds() {
|
|||||||
function toggleFeeling(bondIdx, feeling, el) {
|
function toggleFeeling(bondIdx, feeling, el) {
|
||||||
const bond = bonds[bondIdx];
|
const bond = bonds[bondIdx];
|
||||||
if (bond.feelings.includes(feeling)) {
|
if (bond.feelings.includes(feeling)) {
|
||||||
bond.feelings = bond.feelings.filter(f => f !== feeling);
|
bond.feelings = bond.feelings.filter((f) => f !== feeling);
|
||||||
el.classList.remove('active');
|
el.classList.remove("active");
|
||||||
el.querySelector('.bond-feeling-box').textContent = '';
|
el.querySelector(".bond-feeling-box").textContent = "";
|
||||||
} else {
|
} else {
|
||||||
bond.feelings.push(feeling);
|
bond.feelings.push(feeling);
|
||||||
el.classList.add('active');
|
el.classList.add("active");
|
||||||
el.querySelector('.bond-feeling-box').textContent = '✓';
|
el.querySelector(".bond-feeling-box").textContent = "✓";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── LEVEL ──────────────────────────────────────────
|
// ── LEVEL ──────────────────────────────────────────
|
||||||
function adjustLevel(delta) {
|
function adjustLevel(delta) {
|
||||||
level = Math.max(1, Math.min(50, level + delta));
|
level = Math.max(1, Math.min(50, level + delta));
|
||||||
document.getElementById('levelDisplay').textContent = level;
|
document.getElementById("levelDisplay").textContent = level;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── XP BAR ─────────────────────────────────────────
|
// ── XP BAR ─────────────────────────────────────────
|
||||||
function updateXPBar() {
|
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);
|
const pct = Math.min((xp % 10) * 10, 100);
|
||||||
document.getElementById('xpBar').style.width = pct + '%';
|
document.getElementById("xpBar").style.width = pct + "%";
|
||||||
document.getElementById('xpVal').textContent = xp + ' XP';
|
document.getElementById("xpVal").textContent = xp + " XP";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── STAT CALC ──────────────────────────────────────
|
// ── STAT CALC ──────────────────────────────────────
|
||||||
function recalcStats() { /* auto-calc on keyup */ }
|
function recalcStats() {
|
||||||
|
/* auto-calc on keyup */
|
||||||
|
}
|
||||||
|
|
||||||
function calcHP() {
|
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;
|
const max = mig * 5 + level;
|
||||||
document.getElementById('hpMax').value = max;
|
document.getElementById("hpMax").value = max;
|
||||||
if (!document.getElementById('hpCur').value) document.getElementById('hpCur').value = max;
|
if (!document.getElementById("hpCur").value)
|
||||||
|
document.getElementById("hpCur").value = max;
|
||||||
updateCrisis();
|
updateCrisis();
|
||||||
}
|
}
|
||||||
|
|
||||||
function calcMP() {
|
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;
|
const max = wlp * 5 + level;
|
||||||
document.getElementById('mpMax').value = max;
|
document.getElementById("mpMax").value = max;
|
||||||
if (!document.getElementById('mpCur').value) document.getElementById('mpCur').value = max;
|
if (!document.getElementById("mpCur").value)
|
||||||
|
document.getElementById("mpCur").value = max;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCrisis() {
|
function updateCrisis() {
|
||||||
const max = parseInt(document.getElementById('hpMax').value) || 0;
|
const max = parseInt(document.getElementById("hpMax").value) || 0;
|
||||||
const cur = parseInt(document.getElementById('hpCur').value) || 0;
|
const cur = parseInt(document.getElementById("hpCur").value) || 0;
|
||||||
const badge = document.getElementById('crisisBadge');
|
const badge = document.getElementById("crisisBadge");
|
||||||
badge.style.display = (max > 0 && cur <= Math.floor(max / 2)) ? 'block' : 'none';
|
badge.style.display =
|
||||||
|
max > 0 && cur <= Math.floor(max / 2) ? "block" : "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── MARTIAL ────────────────────────────────────────
|
// ── 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 ────────────────────────────────────
|
// ── 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 ────────────────────────────────────────
|
// ── CLASSES ────────────────────────────────────────
|
||||||
function classBlockHTML(cls, idx, type) {
|
function classBlockHTML(cls, idx, type) {
|
||||||
return `
|
return `
|
||||||
<div class="class-block" id="${type}-class-${idx}">
|
<div class="class-block" id="${type}-class-${idx}">
|
||||||
<div class="class-header">
|
<div class="class-header">
|
||||||
<input type="text" placeholder="Class name…" value="${cls.name || ''}" oninput="${type}Classes[${idx}].name=this.value">
|
<input type="text" placeholder="Class name…" value="${cls.name || ""}" oninput="${type}Classes[${idx}].name=this.value">
|
||||||
<input type="text" placeholder="Free benefits…" value="${cls.benefits || ''}" oninput="${type}Classes[${idx}].benefits=this.value">
|
<input type="text" placeholder="Free benefits…" value="${cls.benefits || ""}" oninput="${type}Classes[${idx}].benefits=this.value">
|
||||||
</div>
|
</div>
|
||||||
<div class="class-skills">
|
<div class="class-skills">
|
||||||
<textarea placeholder="Skill information…" oninput="${type}Classes[${idx}].skills=this.value">${cls.skills || ''}</textarea>
|
<textarea placeholder="Skill information…" oninput="${type}Classes[${idx}].skills=this.value">${cls.skills || ""}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:6px 10px; border-top:1px solid var(--border); display:flex; justify-content:flex-end;">
|
<div style="padding:6px 10px; border-top:1px solid var(--border); display:flex; justify-content:flex-end;">
|
||||||
<button class="spell-del-btn" onclick="removeClass('${type}',${idx})">✕ Remove</button>
|
<button class="spell-del-btn" onclick="removeClass('${type}',${idx})">✕ Remove</button>
|
||||||
@@ -214,38 +256,63 @@ function classBlockHTML(cls, idx, type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderPrimaryClasses() {
|
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() {
|
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 addPrimaryClass() {
|
||||||
function addOtherClass() { otherClasses.push({}); renderOtherClasses(); }
|
primaryClasses.push({});
|
||||||
|
renderPrimaryClasses();
|
||||||
|
}
|
||||||
|
function addOtherClass() {
|
||||||
|
otherClasses.push({});
|
||||||
|
renderOtherClasses();
|
||||||
|
}
|
||||||
function removeClass(type, idx) {
|
function removeClass(type, idx) {
|
||||||
if (type === 'primary') { primaryClasses.splice(idx, 1); renderPrimaryClasses(); }
|
if (type === "primary") {
|
||||||
else { otherClasses.splice(idx, 1); renderOtherClasses(); }
|
primaryClasses.splice(idx, 1);
|
||||||
|
renderPrimaryClasses();
|
||||||
|
} else {
|
||||||
|
otherClasses.splice(idx, 1);
|
||||||
|
renderOtherClasses();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SPELLS ─────────────────────────────────────────
|
// ── SPELLS ─────────────────────────────────────────
|
||||||
function renderSpells() {
|
function renderSpells() {
|
||||||
const tbody = document.getElementById('spellsBody');
|
const tbody = document.getElementById("spellsBody");
|
||||||
tbody.innerHTML = spells.map((s, i) => `
|
tbody.innerHTML = spells
|
||||||
|
.map(
|
||||||
|
(s, i) => `
|
||||||
<tr>
|
<tr>
|
||||||
<td class="spell-name-col">
|
<td class="spell-name-col">
|
||||||
<input type="text" placeholder="Spell / Arcana name…" value="${s.name || ''}" oninput="spells[${i}].name=this.value">
|
<input type="text" placeholder="Spell / Arcana name…" value="${s.name || ""}" oninput="spells[${i}].name=this.value">
|
||||||
<textarea placeholder="Notes / effect description…" oninput="spells[${i}].notes=this.value">${s.notes || ''}</textarea>
|
<textarea placeholder="Notes / effect description…" oninput="spells[${i}].notes=this.value">${s.notes || ""}</textarea>
|
||||||
</td>
|
</td>
|
||||||
<td class="spell-mp-col"><input type="number" placeholder="0" value="${s.mp || ''}" oninput="spells[${i}].mp=this.value" style="min-height:32px;"></td>
|
<td class="spell-mp-col"><input type="number" placeholder="0" value="${s.mp || ""}" oninput="spells[${i}].mp=this.value" style="min-height:32px;"></td>
|
||||||
<td class="spell-targets-col"><input type="text" placeholder="Target(s)…" value="${s.targets || ''}" oninput="spells[${i}].targets=this.value"></td>
|
<td class="spell-targets-col"><input type="text" placeholder="Target(s)…" value="${s.targets || ""}" oninput="spells[${i}].targets=this.value"></td>
|
||||||
<td class="spell-dur-col"><input type="text" placeholder="Duration…" value="${s.duration || ''}" oninput="spells[${i}].duration=this.value"></td>
|
<td class="spell-dur-col"><input type="text" placeholder="Duration…" value="${s.duration || ""}" oninput="spells[${i}].duration=this.value"></td>
|
||||||
<td class="spell-del-col"><button class="spell-del-btn" onclick="removeSpell(${i})">✕</button></td>
|
<td class="spell-del-col"><button class="spell-del-btn" onclick="removeSpell(${i})">✕</button></td>
|
||||||
</tr>`).join('');
|
</tr>`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSpell() { spells.push({}); renderSpells(); }
|
function addSpell() {
|
||||||
function removeSpell(i) { spells.splice(i, 1); renderSpells(); }
|
spells.push({});
|
||||||
|
renderSpells();
|
||||||
|
}
|
||||||
|
function removeSpell(i) {
|
||||||
|
spells.splice(i, 1);
|
||||||
|
renderSpells();
|
||||||
|
}
|
||||||
|
|
||||||
// ── SAVE / LOAD ────────────────────────────────────
|
// ── SAVE / LOAD ────────────────────────────────────
|
||||||
// Short key legend (serialized form only; in-memory objects use full names):
|
// 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 classes: n=name, b=benefits, s=skills
|
||||||
// Nested spells: n=name, nt=notes, mp=mp, tg=targets, dr=duration
|
// Nested spells: n=name, nt=notes, mp=mp, tg=targets, dr=duration
|
||||||
function collectData() {
|
function collectData() {
|
||||||
const get = id => { const el = document.getElementById(id); return el ? el.value : ''; };
|
const get = (id) => {
|
||||||
const sa = [...document.querySelectorAll('.status-item.active-status')].map(el => el.dataset.status);
|
const el = document.getElementById(id);
|
||||||
const ma = [...document.querySelectorAll('.martial-item.checked')].map(el => el.querySelector('span').textContent);
|
return el ? el.value : "";
|
||||||
const da = [...document.querySelectorAll('.disc-item.checked')].map(el => el.querySelector('span').textContent);
|
};
|
||||||
|
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 {
|
return {
|
||||||
n: get('charName'), pn: get('charPronouns'),
|
n: get("charName"),
|
||||||
id: get('charIdentity'), th: get('charTheme'), og: get('charOrigin'),
|
pn: get("charPronouns"),
|
||||||
tr: get('charTraits'),
|
id: get("charIdentity"),
|
||||||
lv: level, xp: get('xpCurrent'), z: get('zenit'),
|
th: get("charTheme"),
|
||||||
im: get('initMod'), df: get('defense'), md: get('magDef'),
|
og: get("charOrigin"),
|
||||||
dxb: get('dex-base'), dxc: get('dex-cur'),
|
tr: get("charTraits"),
|
||||||
inb: get('ins-base'), inc: get('ins-cur'),
|
lv: level,
|
||||||
mgb: get('mig-base'), mgc: get('mig-cur'),
|
xp: get("xpCurrent"),
|
||||||
wpb: get('wlp-base'), wpc: get('wlp-cur'),
|
z: get("zenit"),
|
||||||
hx: get('hpMax'), hc: get('hpCur'),
|
im: get("initMod"),
|
||||||
mx: get('mpMax'), mc: get('mpCur'),
|
df: get("defense"),
|
||||||
ix: get('ipMax'), ic: get('ipCur'),
|
md: get("magDef"),
|
||||||
fp: get('fpCount'),
|
dxb: get("dex-base"),
|
||||||
bp: get('backpack'),
|
dxc: get("dex-cur"),
|
||||||
acn: get('acc-name'), acd: get('acc-desc'),
|
inb: get("ins-base"),
|
||||||
amn: get('arm-name'), amd: get('arm-desc'),
|
inc: get("ins-cur"),
|
||||||
mhn: get('mh-name'), mhd: get('mh-desc'),
|
mgb: get("mig-base"),
|
||||||
ohn: get('oh-name'), ohd: get('oh-desc'),
|
mgc: get("mig-cur"),
|
||||||
hs: get('heroicSkills'),
|
wpb: get("wlp-base"),
|
||||||
rn: get('ritualsNotes'),
|
wpc: get("wlp-cur"),
|
||||||
sa, ma, da,
|
hx: get("hpMax"),
|
||||||
bo: bonds.map(b => ({ n: b.name, f: b.feelings })),
|
hc: get("hpCur"),
|
||||||
pc: primaryClasses.map(c => ({ n: c.name, b: c.benefits, s: c.skills })),
|
mx: get("mpMax"),
|
||||||
oc: otherClasses.map(c => ({ n: c.name, b: c.benefits, s: c.skills })),
|
mc: get("mpCur"),
|
||||||
sp: spells.map(s => ({ n: s.name, nt: s.notes, mp: s.mp, tg: s.targets, dr: s.duration })),
|
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) {
|
function saveSheet(isAuto = false) {
|
||||||
if (isAuto && urlMode) return;
|
if (isAuto && urlMode) return;
|
||||||
const data = collectData();
|
const data = collectData();
|
||||||
localStorage.setItem('fabulaUltimaSheet', JSON.stringify(data));
|
localStorage.setItem("fabulaUltimaSheet", JSON.stringify(data));
|
||||||
const st = document.getElementById('saveStatus');
|
const st = document.getElementById("saveStatus");
|
||||||
st.classList.add('show');
|
st.classList.add("show");
|
||||||
setTimeout(() => st.classList.remove('show'), 2000);
|
setTimeout(() => st.classList.remove("show"), 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryAutoLoad() {
|
function tryAutoLoad() {
|
||||||
const raw = localStorage.getItem('fabulaUltimaSheet');
|
const raw = localStorage.getItem("fabulaUltimaSheet");
|
||||||
if (raw) try { applyData(JSON.parse(raw)); } catch (e) { }
|
if (raw)
|
||||||
|
try {
|
||||||
|
applyData(JSON.parse(raw));
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSheet() {
|
function loadSheet() {
|
||||||
const raw = localStorage.getItem('fabulaUltimaSheet');
|
const raw = localStorage.getItem("fabulaUltimaSheet");
|
||||||
if (!raw) return alert('No saved sheet found.');
|
if (!raw) return alert("No saved sheet found.");
|
||||||
try { applyData(JSON.parse(raw)); } catch (e) { alert('Could not load sheet.'); }
|
try {
|
||||||
|
applyData(JSON.parse(raw));
|
||||||
|
} catch (e) {
|
||||||
|
alert("Could not load sheet.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyData(d) {
|
function applyData(d) {
|
||||||
const set = (id, val) => { const el = document.getElementById(id); if (el && val !== undefined) el.value = val; };
|
const set = (id, val) => {
|
||||||
set('charName', d.n ?? d.name); set('charPronouns', d.pn ?? d.pronouns);
|
const el = document.getElementById(id);
|
||||||
set('charIdentity',d.id ?? d.identity); set('charTheme', d.th ?? d.theme);
|
if (el && val !== undefined) el.value = val;
|
||||||
set('charOrigin', d.og ?? d.origin); set('charTraits', d.tr ?? d.traits);
|
};
|
||||||
|
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;
|
level = d.lv ?? d.level ?? 1;
|
||||||
document.getElementById('levelDisplay').textContent = level;
|
document.getElementById("levelDisplay").textContent = level;
|
||||||
set('xpCurrent', d.xp); set('zenit', d.z ?? d.zenit);
|
set("xpCurrent", d.xp);
|
||||||
set('initMod', d.im ?? d.initMod); set('defense', d.df ?? d.defense);
|
set("zenit", d.z ?? d.zenit);
|
||||||
set('magDef', d.md ?? d.magDef);
|
set("initMod", d.im ?? d.initMod);
|
||||||
set('dex-base', d.dxb ?? d.dexBase); set('dex-cur', d.dxc ?? d.dexCur);
|
set("defense", d.df ?? d.defense);
|
||||||
set('ins-base', d.inb ?? d.insBase); set('ins-cur', d.inc ?? d.insCur);
|
set("magDef", d.md ?? d.magDef);
|
||||||
set('mig-base', d.mgb ?? d.migBase); set('mig-cur', d.mgc ?? d.migCur);
|
set("dex-base", d.dxb ?? d.dexBase);
|
||||||
set('wlp-base', d.wpb ?? d.wlpBase); set('wlp-cur', d.wpc ?? d.wlpCur);
|
set("dex-cur", d.dxc ?? d.dexCur);
|
||||||
set('hpMax', d.hx ?? d.hpMax); set('hpCur', d.hc ?? d.hpCur);
|
set("ins-base", d.inb ?? d.insBase);
|
||||||
set('mpMax', d.mx ?? d.mpMax); set('mpCur', d.mc ?? d.mpCur);
|
set("ins-cur", d.inc ?? d.insCur);
|
||||||
set('ipMax', d.ix ?? d.ipMax); set('ipCur', d.ic ?? d.ipCur);
|
set("mig-base", d.mgb ?? d.migBase);
|
||||||
set('fpCount', d.fp);
|
set("mig-cur", d.mgc ?? d.migCur);
|
||||||
set('backpack', d.bp ?? d.backpack);
|
set("wlp-base", d.wpb ?? d.wlpBase);
|
||||||
set('acc-name', d.acn ?? d.accName); set('acc-desc', d.acd ?? d.accDesc);
|
set("wlp-cur", d.wpc ?? d.wlpCur);
|
||||||
set('arm-name', d.amn ?? d.armName); set('arm-desc', d.amd ?? d.armDesc);
|
set("hpMax", d.hx ?? d.hpMax);
|
||||||
set('mh-name', d.mhn ?? d.mhName); set('mh-desc', d.mhd ?? d.mhDesc);
|
set("hpCur", d.hc ?? d.hpCur);
|
||||||
set('oh-name', d.ohn ?? d.ohName); set('oh-desc', d.ohd ?? d.ohDesc);
|
set("mpMax", d.mx ?? d.mpMax);
|
||||||
set('heroicSkills', d.hs ?? d.heroicSkills);
|
set("mpCur", d.mc ?? d.mpCur);
|
||||||
set('ritualsNotes', d.rn ?? d.ritualsNotes);
|
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
|
// Statuses
|
||||||
const sa = d.sa ?? d.statusesActive ?? [];
|
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);
|
const active = sa.includes(el.dataset.status);
|
||||||
el.classList.toggle('active-status', active);
|
el.classList.toggle("active-status", active);
|
||||||
el.querySelector('.status-check').textContent = active ? '✗' : '';
|
el.querySelector(".status-check").textContent = active ? "✗" : "";
|
||||||
});
|
});
|
||||||
|
|
||||||
// Martial
|
// Martial
|
||||||
const ma = d.ma ?? d.martialChecked ?? [];
|
const ma = d.ma ?? d.martialChecked ?? [];
|
||||||
document.querySelectorAll('.martial-item').forEach(el => {
|
document.querySelectorAll(".martial-item").forEach((el) => {
|
||||||
const checked = ma.includes(el.querySelector('span').textContent);
|
const checked = ma.includes(el.querySelector("span").textContent);
|
||||||
el.classList.toggle('checked', checked);
|
el.classList.toggle("checked", checked);
|
||||||
el.querySelector('.martial-box').textContent = checked ? '✓' : '';
|
el.querySelector(".martial-box").textContent = checked ? "✓" : "";
|
||||||
});
|
});
|
||||||
|
|
||||||
// Disciplines
|
// Disciplines
|
||||||
const da = d.da ?? d.disciplinesChecked ?? [];
|
const da = d.da ?? d.disciplinesChecked ?? [];
|
||||||
document.querySelectorAll('.disc-item').forEach(el => {
|
document.querySelectorAll(".disc-item").forEach((el) => {
|
||||||
const checked = da.includes(el.querySelector('span').textContent);
|
const checked = da.includes(el.querySelector("span").textContent);
|
||||||
el.classList.toggle('checked', checked);
|
el.classList.toggle("checked", checked);
|
||||||
el.querySelector('.disc-box').textContent = checked ? '✓' : '';
|
el.querySelector(".disc-box").textContent = checked ? "✓" : "";
|
||||||
});
|
});
|
||||||
|
|
||||||
const rawBonds = d.bo ?? d.bonds;
|
const rawBonds = d.bo ?? d.bonds;
|
||||||
if (rawBonds) {
|
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();
|
renderBonds();
|
||||||
}
|
}
|
||||||
const rawPrimary = d.pc ?? d.primaryClasses;
|
const rawPrimary = d.pc ?? d.primaryClasses;
|
||||||
if (rawPrimary) {
|
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();
|
renderPrimaryClasses();
|
||||||
}
|
}
|
||||||
const rawOther = d.oc ?? d.otherClasses;
|
const rawOther = d.oc ?? d.otherClasses;
|
||||||
if (rawOther) {
|
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();
|
renderOtherClasses();
|
||||||
}
|
}
|
||||||
const rawSpells = d.sp ?? d.spells;
|
const rawSpells = d.sp ?? d.spells;
|
||||||
if (rawSpells) {
|
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();
|
renderSpells();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,36 +544,41 @@ function applyData(d) {
|
|||||||
function exportSheet() {
|
function exportSheet() {
|
||||||
const data = collectData();
|
const data = collectData();
|
||||||
const json = JSON.stringify(data, null, 2);
|
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 url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement("a");
|
||||||
const charName = (data.n || 'character').replace(/[^a-z0-9_\- ]/gi, '').trim() || 'character';
|
const charName =
|
||||||
|
(data.n || "character").replace(/[^a-z0-9_\- ]/gi, "").trim() ||
|
||||||
|
"character";
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = charName + '-fabula-ultima.json';
|
a.download = charName + "-fabula-ultima.json";
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function importSheet() {
|
function importSheet() {
|
||||||
document.getElementById('importFileInput').value = '';
|
document.getElementById("importFileInput").value = "";
|
||||||
document.getElementById('importFileInput').click();
|
document.getElementById("importFileInput").click();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleImportFile(input) {
|
function handleImportFile(input) {
|
||||||
const file = input.files[0];
|
const file = input.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = e => {
|
reader.onload = (e) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(e.target.result);
|
const data = JSON.parse(e.target.result);
|
||||||
applyData(data);
|
applyData(data);
|
||||||
saveSheet();
|
saveSheet();
|
||||||
const st = document.getElementById('saveStatus');
|
const st = document.getElementById("saveStatus");
|
||||||
st.textContent = 'Imported!';
|
st.textContent = "Imported!";
|
||||||
st.classList.add('show');
|
st.classList.add("show");
|
||||||
setTimeout(() => { st.classList.remove('show'); st.textContent = 'Saved!'; }, 2500);
|
setTimeout(() => {
|
||||||
|
st.classList.remove("show");
|
||||||
|
st.textContent = "Saved!";
|
||||||
|
}, 2500);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Could not import: invalid JSON file.');
|
alert("Could not import: invalid JSON file.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
@@ -437,20 +587,26 @@ function handleImportFile(input) {
|
|||||||
async function copyShareURL() {
|
async function copyShareURL() {
|
||||||
const json = JSON.stringify(collectData());
|
const json = JSON.stringify(collectData());
|
||||||
const encoded = await compressToBase64(json);
|
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);
|
await navigator.clipboard.writeText(url);
|
||||||
const st = document.getElementById('copyStatus');
|
const st = document.getElementById("copyStatus");
|
||||||
st.classList.add('show');
|
st.classList.add("show");
|
||||||
setTimeout(() => st.classList.remove('show'), 2000);
|
setTimeout(() => st.classList.remove("show"), 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── THEME ──────────────────────────────────────────
|
// ── THEME ──────────────────────────────────────────
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
const html = document.documentElement;
|
const html = document.documentElement;
|
||||||
const goLight = html.dataset.theme !== 'light';
|
const goLight = html.dataset.theme !== "light";
|
||||||
html.dataset.theme = goLight ? 'light' : 'dark';
|
html.dataset.theme = goLight ? "light" : "dark";
|
||||||
document.getElementById('themeToggle').textContent = goLight ? '☾ Dark' : '☀ Light';
|
document.getElementById("themeToggle").textContent = goLight
|
||||||
localStorage.setItem('fabulaUltimaTheme', html.dataset.theme);
|
? "☾ Dark"
|
||||||
|
: "☀ Light";
|
||||||
|
localStorage.setItem("fabulaUltimaTheme", html.dataset.theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-save every 30s
|
// Auto-save every 30s
|
||||||
|
|||||||
27
package-lock.json
generated
Normal file
27
package-lock.json
generated
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
package.json
Normal file
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"prettier": "^3.8.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user