1359 lines
49 KiB
HTML
1359 lines
49 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<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">
|
||
<style>
|
||
:root {
|
||
--bg: #0d0f0e;
|
||
--surface: #141816;
|
||
--surface2: #1b1f1d;
|
||
--surface3: #222826;
|
||
--border: #2e3830;
|
||
--border-bright: #3d4f47;
|
||
--teal: #4ecdc4;
|
||
--teal-dim: #2a6b66;
|
||
--gold: #c9a84c;
|
||
--gold-dim: #7a6130;
|
||
--red: #c0392b;
|
||
--text: #d4cfc4;
|
||
--text-dim: #7a8a82;
|
||
--text-bright: #ede8dc;
|
||
--crisis: #e74c3c;
|
||
--font-display: 'Cinzel', serif;
|
||
--font-body: 'Crimson Text', serif;
|
||
--font-mono: 'Inconsolata', monospace;
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-family: var(--font-body);
|
||
font-size: 16px;
|
||
min-height: 100vh;
|
||
background-image:
|
||
radial-gradient(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 {
|
||
background: linear-gradient(135deg, var(--surface) 0%, #0f1614 100%);
|
||
border-bottom: 1px solid var(--border-bright);
|
||
padding: 20px 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
box-shadow: 0 4px 24px rgba(0,0,0,0.5);
|
||
}
|
||
|
||
.logo {
|
||
font-family: var(--font-display);
|
||
font-size: 1.1rem;
|
||
color: var(--teal);
|
||
letter-spacing: 0.15em;
|
||
text-transform: uppercase;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.logo::before {
|
||
content: '✦';
|
||
color: var(--gold);
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 4px;
|
||
}
|
||
.tab {
|
||
font-family: var(--font-display);
|
||
font-size: 0.65rem;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
padding: 7px 16px;
|
||
border: 1px solid var(--border);
|
||
background: transparent;
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.tab:hover { border-color: var(--teal-dim); color: var(--teal); }
|
||
.tab.active { background: var(--teal-dim); border-color: var(--teal); color: var(--text-bright); }
|
||
|
||
/* ── LAYOUT ──────────────────────────────────────── */
|
||
.page { display: none; padding: 28px 32px; max-width: 1400px; margin: 0 auto; }
|
||
.page.active { display: block; animation: fadeIn 0.3s ease; }
|
||
|
||
@keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
||
|
||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; }
|
||
.col-span-2 { grid-column: span 2; }
|
||
|
||
/* ── SECTION ─────────────────────────────────────── */
|
||
.section {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
padding: 18px 20px;
|
||
position: relative;
|
||
}
|
||
.section::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0; left: 0;
|
||
width: 3px; height: 100%;
|
||
background: var(--teal-dim);
|
||
}
|
||
|
||
.section-title {
|
||
font-family: var(--font-display);
|
||
font-size: 0.6rem;
|
||
letter-spacing: 0.2em;
|
||
text-transform: uppercase;
|
||
color: var(--teal);
|
||
margin-bottom: 14px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.section-title .icon { color: var(--gold); }
|
||
|
||
/* ── FIELDS ──────────────────────────────────────── */
|
||
.field { margin-bottom: 14px; }
|
||
.field:last-child { margin-bottom: 0; }
|
||
|
||
label {
|
||
display: block;
|
||
font-family: var(--font-display);
|
||
font-size: 0.55rem;
|
||
letter-spacing: 0.18em;
|
||
text-transform: uppercase;
|
||
color: var(--text-dim);
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
input[type="text"], input[type="number"], textarea, select {
|
||
width: 100%;
|
||
background: var(--surface2);
|
||
border: 1px solid var(--border);
|
||
color: var(--text-bright);
|
||
font-family: var(--font-body);
|
||
font-size: 1rem;
|
||
padding: 8px 11px;
|
||
outline: none;
|
||
transition: border-color 0.2s, box-shadow 0.2s;
|
||
-webkit-appearance: none;
|
||
}
|
||
input[type="text"]:focus, input[type="number"]:focus, textarea:focus, select:focus {
|
||
border-color: var(--teal);
|
||
box-shadow: 0 0 0 2px rgba(78,205,196,0.12);
|
||
}
|
||
textarea { resize: vertical; min-height: 70px; }
|
||
input[type="number"] { font-family: var(--font-mono); }
|
||
|
||
.field-row { display: flex; gap: 12px; }
|
||
.field-row .field { flex: 1; }
|
||
|
||
/* ── ATTRIBUTES ──────────────────────────────────── */
|
||
.attr-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||
|
||
.attr-block {
|
||
background: var(--surface2);
|
||
border: 1px solid var(--border);
|
||
padding: 12px 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.attr-name {
|
||
font-family: var(--font-display);
|
||
font-size: 0.65rem;
|
||
letter-spacing: 0.15em;
|
||
text-transform: uppercase;
|
||
color: var(--gold);
|
||
}
|
||
|
||
.attr-inputs { display: flex; gap: 8px; align-items: center; }
|
||
.attr-inputs input { font-family: var(--font-mono); font-size: 1.2rem; text-align: center; width: 52px; flex-shrink: 0; }
|
||
.attr-sep { color: var(--text-dim); font-size: 0.8rem; }
|
||
|
||
/* ── STATUS EFFECTS ──────────────────────────────── */
|
||
.status-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||
|
||
.status-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
cursor: pointer;
|
||
padding: 7px 10px;
|
||
border: 1px solid var(--border);
|
||
background: var(--surface2);
|
||
transition: all 0.15s;
|
||
user-select: none;
|
||
}
|
||
.status-item:hover { border-color: var(--border-bright); }
|
||
.status-item.active-status { border-color: var(--red); background: rgba(192,57,43,0.12); }
|
||
|
||
.status-check {
|
||
width: 14px; height: 14px;
|
||
border: 1px solid var(--border-bright);
|
||
flex-shrink: 0;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 0.7rem;
|
||
color: var(--red);
|
||
transition: all 0.15s;
|
||
}
|
||
.status-item.active-status .status-check { border-color: var(--red); background: rgba(192,57,43,0.2); }
|
||
|
||
.status-label {
|
||
font-family: var(--font-display);
|
||
font-size: 0.58rem;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
color: var(--text-dim);
|
||
}
|
||
.status-item.active-status .status-label { color: var(--crisis); }
|
||
|
||
/* ── HP / MP / IP ────────────────────────────────── */
|
||
.vital-block {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
background: var(--surface2);
|
||
border: 1px solid var(--border);
|
||
padding: 14px 16px;
|
||
margin-bottom: 10px;
|
||
}
|
||
.vital-block:last-child { margin-bottom: 0; }
|
||
|
||
.vital-label {
|
||
font-family: var(--font-display);
|
||
font-size: 1.2rem;
|
||
font-weight: 700;
|
||
width: 32px;
|
||
flex-shrink: 0;
|
||
}
|
||
.vital-label.hp { color: var(--red); }
|
||
.vital-label.mp { color: var(--teal); }
|
||
.vital-label.ip { color: var(--gold); }
|
||
|
||
.vital-inputs { display: flex; gap: 8px; flex: 1; align-items: center; }
|
||
.vital-inputs input { text-align: center; font-family: var(--font-mono); font-size: 1.1rem; }
|
||
.vital-sep { color: var(--text-dim); font-size: 0.8rem; flex-shrink: 0; }
|
||
|
||
.vital-formula {
|
||
font-size: 0.75rem;
|
||
color: var(--text-dim);
|
||
font-family: var(--font-mono);
|
||
}
|
||
|
||
.crisis-badge {
|
||
font-family: var(--font-display);
|
||
font-size: 0.5rem;
|
||
letter-spacing: 0.15em;
|
||
text-transform: uppercase;
|
||
color: var(--crisis);
|
||
border: 1px solid var(--crisis);
|
||
padding: 2px 6px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ── LEVEL / XP ──────────────────────────────────── */
|
||
.level-display {
|
||
text-align: center;
|
||
padding: 20px;
|
||
background: var(--surface2);
|
||
border: 1px solid var(--border);
|
||
}
|
||
.level-num {
|
||
font-family: var(--font-display);
|
||
font-size: 3.5rem;
|
||
color: var(--gold);
|
||
line-height: 1;
|
||
display: block;
|
||
}
|
||
.level-text {
|
||
font-family: var(--font-display);
|
||
font-size: 0.55rem;
|
||
letter-spacing: 0.25em;
|
||
color: var(--text-dim);
|
||
text-transform: uppercase;
|
||
display: block;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.xp-bar-wrap {
|
||
margin-top: 14px;
|
||
background: var(--surface3);
|
||
border: 1px solid var(--border);
|
||
height: 8px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.xp-bar {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, var(--gold-dim), var(--gold));
|
||
transition: width 0.5s ease;
|
||
}
|
||
.xp-label {
|
||
font-family: var(--font-mono);
|
||
font-size: 0.75rem;
|
||
color: var(--text-dim);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
/* ── FABULA POINTS ───────────────────────────────── */
|
||
.fp-pips {
|
||
display: flex;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
margin-top: 10px;
|
||
}
|
||
.fp-pip {
|
||
width: 28px; height: 28px;
|
||
border: 2px solid var(--border-bright);
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
background: var(--surface2);
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.fp-pip.filled { background: var(--teal); border-color: var(--teal); box-shadow: 0 0 8px rgba(78,205,196,0.4); }
|
||
.fp-pip:hover { border-color: var(--teal); }
|
||
|
||
.fp-rules {
|
||
margin-top: 14px;
|
||
border-top: 1px solid var(--border);
|
||
padding-top: 12px;
|
||
}
|
||
.fp-rule {
|
||
font-size: 0.85rem;
|
||
color: var(--text-dim);
|
||
padding: 3px 0;
|
||
padding-left: 12px;
|
||
position: relative;
|
||
}
|
||
.fp-rule::before { content: '✦'; position: absolute; left: 0; font-size: 0.5rem; color: var(--gold); top: 5px; }
|
||
.fp-rule strong { color: var(--text); }
|
||
|
||
/* ── BONDS ───────────────────────────────────────── */
|
||
.bond-block {
|
||
border: 1px solid var(--border);
|
||
padding: 14px;
|
||
background: var(--surface2);
|
||
margin-bottom: 10px;
|
||
}
|
||
.bond-block:last-child { margin-bottom: 0; }
|
||
|
||
.bond-header { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; }
|
||
.bond-num {
|
||
font-family: var(--font-display);
|
||
font-size: 0.6rem;
|
||
color: var(--gold);
|
||
border: 1px solid var(--gold-dim);
|
||
width: 22px; height: 22px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
.bond-header input { flex: 1; }
|
||
|
||
.bond-feelings {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 5px;
|
||
}
|
||
.bond-feeling {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
cursor: pointer;
|
||
font-size: 0.8rem;
|
||
color: var(--text-dim);
|
||
user-select: none;
|
||
}
|
||
.bond-feeling.active { color: var(--teal); }
|
||
.bond-feeling-box {
|
||
width: 12px; height: 12px;
|
||
border: 1px solid var(--border-bright);
|
||
flex-shrink: 0;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 0.6rem;
|
||
transition: all 0.15s;
|
||
}
|
||
.bond-feeling.active .bond-feeling-box { background: var(--teal); border-color: var(--teal); color: #000; }
|
||
|
||
/* ── EQUIPMENT ───────────────────────────────────── */
|
||
.equip-row {
|
||
display: grid;
|
||
grid-template-columns: 120px 1fr;
|
||
gap: 0;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.equip-row:last-child { border-bottom: none; }
|
||
|
||
.equip-slot {
|
||
font-family: var(--font-display);
|
||
font-size: 0.55rem;
|
||
letter-spacing: 0.15em;
|
||
text-transform: uppercase;
|
||
color: var(--gold);
|
||
background: var(--surface3);
|
||
padding: 10px 12px;
|
||
border-right: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.equip-fields { display: flex; flex-direction: column; gap: 0; }
|
||
.equip-fields input {
|
||
border: none;
|
||
border-bottom: 1px solid var(--border);
|
||
font-size: 0.9rem;
|
||
padding: 7px 10px;
|
||
}
|
||
.equip-fields input:last-child { border-bottom: none; }
|
||
.equip-fields input::placeholder { color: var(--border-bright); font-style: italic; }
|
||
|
||
/* ── MARTIAL CHECKBOXES ──────────────────────────── */
|
||
.martial-row {
|
||
display: flex;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
margin-top: 10px;
|
||
}
|
||
.martial-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
.martial-box {
|
||
width: 14px; height: 14px;
|
||
border: 1px solid var(--border-bright);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 0.65rem;
|
||
color: var(--teal);
|
||
transition: all 0.15s;
|
||
}
|
||
.martial-item.checked .martial-box { background: var(--teal-dim); border-color: var(--teal); }
|
||
.martial-item span {
|
||
font-family: var(--font-display);
|
||
font-size: 0.55rem;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
/* ── CLASSES ─────────────────────────────────────── */
|
||
.class-block {
|
||
border: 1px solid var(--border);
|
||
background: var(--surface2);
|
||
margin-bottom: 12px;
|
||
}
|
||
.class-block:last-child { margin-bottom: 0; }
|
||
|
||
.class-header {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.class-header input {
|
||
border: none;
|
||
border-right: 1px solid var(--border);
|
||
font-size: 0.9rem;
|
||
}
|
||
.class-header input:last-child { border-right: none; }
|
||
|
||
.class-skills { padding: 0; }
|
||
.class-skills textarea {
|
||
border: none;
|
||
font-size: 0.85rem;
|
||
min-height: 80px;
|
||
background: transparent;
|
||
}
|
||
|
||
/* ── SPELLS TABLE ────────────────────────────────── */
|
||
.spells-table { width: 100%; border-collapse: collapse; }
|
||
.spells-table thead tr {
|
||
background: var(--surface3);
|
||
}
|
||
.spells-table th {
|
||
font-family: var(--font-display);
|
||
font-size: 0.52rem;
|
||
letter-spacing: 0.15em;
|
||
text-transform: uppercase;
|
||
color: var(--teal);
|
||
padding: 8px 10px;
|
||
text-align: left;
|
||
border-bottom: 1px solid var(--border-bright);
|
||
}
|
||
.spells-table td { border-bottom: 1px solid var(--border); vertical-align: top; }
|
||
.spells-table td input, .spells-table td textarea {
|
||
border: none;
|
||
font-size: 0.9rem;
|
||
min-height: 32px;
|
||
padding: 6px 8px;
|
||
}
|
||
.spells-table td textarea { min-height: 55px; }
|
||
.spell-name-col { width: 35%; }
|
||
.spell-mp-col { width: 12%; }
|
||
.spell-targets-col { width: 18%; }
|
||
.spell-dur-col { width: 18%; }
|
||
.spell-del-col { width: 40px; }
|
||
.spell-del-btn {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
padding: 6px 8px;
|
||
font-size: 0.9rem;
|
||
transition: color 0.15s;
|
||
}
|
||
.spell-del-btn:hover { color: var(--red); }
|
||
|
||
.add-btn {
|
||
margin-top: 8px;
|
||
font-family: var(--font-display);
|
||
font-size: 0.55rem;
|
||
letter-spacing: 0.15em;
|
||
text-transform: uppercase;
|
||
color: var(--teal);
|
||
background: transparent;
|
||
border: 1px solid var(--teal-dim);
|
||
padding: 6px 14px;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.add-btn:hover { background: var(--teal-dim); color: var(--text-bright); }
|
||
|
||
/* ── DISCIPLINES ─────────────────────────────────── */
|
||
.disciplines-row {
|
||
display: flex;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 14px;
|
||
}
|
||
.disc-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
.disc-box {
|
||
width: 13px; height: 13px;
|
||
border: 1px solid var(--border-bright);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 0.6rem;
|
||
color: var(--gold);
|
||
transition: all 0.15s;
|
||
}
|
||
.disc-item.checked .disc-box { background: var(--gold-dim); border-color: var(--gold); }
|
||
.disc-item span {
|
||
font-family: var(--font-display);
|
||
font-size: 0.55rem;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: var(--text-dim);
|
||
}
|
||
.disc-item.checked span { color: var(--gold); }
|
||
|
||
/* ── SAVE / LOAD ─────────────────────────────────── */
|
||
.toolbar {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
.btn-save, .btn-load {
|
||
font-family: var(--font-display);
|
||
font-size: 0.55rem;
|
||
letter-spacing: 0.15em;
|
||
text-transform: uppercase;
|
||
padding: 7px 16px;
|
||
cursor: pointer;
|
||
border: 1px solid var(--gold-dim);
|
||
background: transparent;
|
||
color: var(--gold);
|
||
transition: all 0.2s;
|
||
}
|
||
.btn-save:hover { background: var(--gold-dim); color: var(--text-bright); }
|
||
.btn-load { border-color: var(--border-bright); color: var(--text-dim); }
|
||
.btn-load:hover { border-color: var(--teal-dim); color: var(--teal); }
|
||
|
||
.save-status {
|
||
font-family: var(--font-mono);
|
||
font-size: 0.7rem;
|
||
color: var(--teal);
|
||
opacity: 0;
|
||
transition: opacity 0.3s;
|
||
}
|
||
.save-status.show { opacity: 1; }
|
||
|
||
/* ── ORNAMENTS ───────────────────────────────────── */
|
||
.ornament {
|
||
text-align: center;
|
||
color: var(--gold-dim);
|
||
font-size: 0.8rem;
|
||
letter-spacing: 0.5em;
|
||
margin: 8px 0;
|
||
}
|
||
|
||
.def-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 10px;
|
||
margin-bottom: 14px;
|
||
}
|
||
.def-block {
|
||
background: var(--surface2);
|
||
border: 1px solid var(--border);
|
||
padding: 10px 12px;
|
||
text-align: center;
|
||
}
|
||
.def-block label { display: block; margin-bottom: 6px; }
|
||
.def-block input { text-align: center; font-family: var(--font-mono); font-size: 1.2rem; font-weight: 600; }
|
||
|
||
/* ── RESPONSIVE ──────────────────────────────────── */
|
||
@media (max-width: 900px) {
|
||
.grid-2, .grid-3 { grid-template-columns: 1fr; }
|
||
.col-span-2 { grid-column: span 1; }
|
||
header { flex-direction: column; gap: 12px; }
|
||
.tabs { flex-wrap: wrap; }
|
||
}
|
||
|
||
/* ── TOOLTIP ─────────────────────────────────────── */
|
||
[title] { cursor: help; }
|
||
|
||
/* scrollbar */
|
||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||
::-webkit-scrollbar-track { background: var(--bg); }
|
||
::-webkit-scrollbar-thumb { background: var(--border-bright); }
|
||
::-webkit-scrollbar-thumb:hover { background: var(--teal-dim); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<div class="logo">Fabula Ultima — Character Sheet</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>
|
||
</div>
|
||
<div class="toolbar">
|
||
<button class="btn-save" onclick="saveSheet()">✦ Save</button>
|
||
<button class="btn-load" onclick="loadSheet()">↑ Load</button>
|
||
<span class="save-status" id="saveStatus">Saved!</span>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- ══════════════════════════════════════════════════
|
||
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>
|
||
|
||
<script>
|
||
// ── STATE ──────────────────────────────────────────
|
||
let level = 1;
|
||
let fpCount = 0;
|
||
let primaryClasses = [];
|
||
let otherClasses = [];
|
||
let spells = [];
|
||
let bonds = [
|
||
{ name: '', feelings: [] },
|
||
{ name: '', feelings: [] },
|
||
{ name: '', feelings: [] },
|
||
{ name: '', feelings: [] },
|
||
{ name: '', feelings: [] },
|
||
{ name: '', feelings: [] },
|
||
];
|
||
|
||
const STATUSES = ['Slow','Enraged','Dazed','Weak','Poisoned','Shaken'];
|
||
const FEELINGS = ['Admiration','Inferiority','Loyalty','Mistrust','Affection','Hatred'];
|
||
|
||
// ── TABS ───────────────────────────────────────────
|
||
function switchTab(tab) {
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
||
event.currentTarget.classList.add('active');
|
||
document.getElementById('page-' + tab).classList.add('active');
|
||
}
|
||
|
||
// ── INIT ───────────────────────────────────────────
|
||
function init() {
|
||
renderStatuses();
|
||
renderFP();
|
||
renderBonds();
|
||
renderPrimaryClasses();
|
||
renderOtherClasses();
|
||
renderSpells();
|
||
updateXPBar();
|
||
tryAutoLoad();
|
||
}
|
||
|
||
// ── STATUS EFFECTS ─────────────────────────────────
|
||
function renderStatuses() {
|
||
const g = document.getElementById('statusGrid');
|
||
g.innerHTML = '';
|
||
STATUSES.forEach(s => {
|
||
const el = document.createElement('div');
|
||
el.className = 'status-item';
|
||
el.dataset.status = s;
|
||
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>`;
|
||
g.appendChild(el);
|
||
});
|
||
}
|
||
|
||
// ── FABULA POINTS ──────────────────────────────────
|
||
function renderFP() {
|
||
fpCount = parseInt(document.getElementById('fpCount').value) || 0;
|
||
const pips = document.getElementById('fpPips');
|
||
const total = Math.max(10, fpCount);
|
||
pips.innerHTML = '';
|
||
for (let i = 0; i < total; i++) {
|
||
const pip = document.createElement('div');
|
||
pip.className = 'fp-pip' + (i < fpCount ? ' filled' : '');
|
||
pip.onclick = () => {
|
||
const newVal = i < fpCount ? i : i + 1;
|
||
document.getElementById('fpCount').value = newVal;
|
||
renderFP();
|
||
};
|
||
pips.appendChild(pip);
|
||
}
|
||
}
|
||
|
||
// ── BONDS ──────────────────────────────────────────
|
||
function renderBonds() {
|
||
const container = document.getElementById('bondsContainer');
|
||
container.innerHTML = '';
|
||
bonds.forEach((bond, idx) => {
|
||
const el = document.createElement('div');
|
||
el.className = 'bond-block';
|
||
el.innerHTML = `
|
||
<div class="bond-header">
|
||
<div class="bond-num">${idx + 1}</div>
|
||
<input type="text" placeholder="Bond target name…" value="${bond.name}" oninput="bonds[${idx}].name=this.value">
|
||
</div>
|
||
<div class="bond-feelings">
|
||
${FEELINGS.map(f => `
|
||
<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>
|
||
</div>`).join('')}
|
||
</div>`;
|
||
container.appendChild(el);
|
||
});
|
||
}
|
||
|
||
function toggleFeeling(bondIdx, feeling, el) {
|
||
const bond = bonds[bondIdx];
|
||
if (bond.feelings.includes(feeling)) {
|
||
bond.feelings = bond.feelings.filter(f => f !== feeling);
|
||
el.classList.remove('active');
|
||
el.querySelector('.bond-feeling-box').textContent = '';
|
||
} else {
|
||
bond.feelings.push(feeling);
|
||
el.classList.add('active');
|
||
el.querySelector('.bond-feeling-box').textContent = '✓';
|
||
}
|
||
}
|
||
|
||
// ── LEVEL ──────────────────────────────────────────
|
||
function adjustLevel(delta) {
|
||
level = Math.max(1, Math.min(50, level + delta));
|
||
document.getElementById('levelDisplay').textContent = level;
|
||
}
|
||
|
||
// ── XP BAR ─────────────────────────────────────────
|
||
function updateXPBar() {
|
||
const xp = parseInt(document.getElementById('xpCurrent').value) || 0;
|
||
const pct = Math.min((xp % 10) * 10, 100);
|
||
document.getElementById('xpBar').style.width = pct + '%';
|
||
document.getElementById('xpVal').textContent = xp + ' XP';
|
||
}
|
||
|
||
// ── STAT CALC ──────────────────────────────────────
|
||
function recalcStats() { /* auto-calc on keyup */ }
|
||
|
||
function calcHP() {
|
||
const mig = parseInt(document.getElementById('mig-base').value) || 6;
|
||
const max = mig * 5 + level;
|
||
document.getElementById('hpMax').value = max;
|
||
if (!document.getElementById('hpCur').value) document.getElementById('hpCur').value = max;
|
||
updateCrisis();
|
||
}
|
||
|
||
function calcMP() {
|
||
const wlp = parseInt(document.getElementById('wlp-base').value) || 6;
|
||
const max = wlp * 5 + level;
|
||
document.getElementById('mpMax').value = max;
|
||
if (!document.getElementById('mpCur').value) document.getElementById('mpCur').value = max;
|
||
}
|
||
|
||
function updateCrisis() {
|
||
const max = parseInt(document.getElementById('hpMax').value) || 0;
|
||
const cur = parseInt(document.getElementById('hpCur').value) || 0;
|
||
const badge = document.getElementById('crisisBadge');
|
||
badge.style.display = (max > 0 && cur <= Math.floor(max / 2)) ? 'block' : 'none';
|
||
}
|
||
|
||
// ── MARTIAL ────────────────────────────────────────
|
||
function toggleMartial(el) { el.classList.toggle('checked'); el.querySelector('.martial-box').textContent = el.classList.contains('checked') ? '✓' : ''; }
|
||
|
||
// ── DISCIPLINES ────────────────────────────────────
|
||
function toggleDisc(el) { el.classList.toggle('checked'); el.querySelector('.disc-box').textContent = el.classList.contains('checked') ? '✓' : ''; }
|
||
|
||
// ── CLASSES ────────────────────────────────────────
|
||
function classBlockHTML(cls, idx, type) {
|
||
return `
|
||
<div class="class-block" id="${type}-class-${idx}">
|
||
<div class="class-header">
|
||
<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">
|
||
</div>
|
||
<div class="class-skills">
|
||
<textarea placeholder="Skill information…" oninput="${type}Classes[${idx}].skills=this.value">${cls.skills||''}</textarea>
|
||
</div>
|
||
<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>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderPrimaryClasses() {
|
||
document.getElementById('primaryClassContainer').innerHTML = primaryClasses.map((c,i) => classBlockHTML(c,i,'primary')).join('');
|
||
}
|
||
|
||
function renderOtherClasses() {
|
||
document.getElementById('otherClassContainer').innerHTML = otherClasses.map((c,i) => classBlockHTML(c,i,'other')).join('');
|
||
}
|
||
|
||
function addPrimaryClass() { primaryClasses.push({}); renderPrimaryClasses(); }
|
||
function addOtherClass() { otherClasses.push({}); renderOtherClasses(); }
|
||
function removeClass(type, idx) {
|
||
if (type === 'primary') { primaryClasses.splice(idx,1); renderPrimaryClasses(); }
|
||
else { otherClasses.splice(idx,1); renderOtherClasses(); }
|
||
}
|
||
|
||
// ── SPELLS ─────────────────────────────────────────
|
||
function renderSpells() {
|
||
const tbody = document.getElementById('spellsBody');
|
||
tbody.innerHTML = spells.map((s,i) => `
|
||
<tr>
|
||
<td class="spell-name-col">
|
||
<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>
|
||
</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-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>
|
||
</tr>`).join('');
|
||
}
|
||
|
||
function addSpell() { spells.push({}); renderSpells(); }
|
||
function removeSpell(i) { spells.splice(i,1); renderSpells(); }
|
||
|
||
// ── SAVE / LOAD ────────────────────────────────────
|
||
function collectData() {
|
||
const get = id => { const el = document.getElementById(id); return el ? el.value : ''; };
|
||
const statusesActive = [...document.querySelectorAll('.status-item.active-status')].map(el => el.dataset.status);
|
||
const martialChecked = [...document.querySelectorAll('.martial-item.checked')].map(el => el.querySelector('span').textContent);
|
||
const disciplinesChecked = [...document.querySelectorAll('.disc-item.checked')].map(el => el.querySelector('span').textContent);
|
||
|
||
return {
|
||
name: get('charName'), pronouns: get('charPronouns'),
|
||
identity: get('charIdentity'), theme: get('charTheme'), origin: get('charOrigin'),
|
||
traits: get('charTraits'),
|
||
level, xp: get('xpCurrent'), zenit: get('zenit'),
|
||
initMod: get('initMod'), defense: get('defense'), magDef: get('magDef'),
|
||
dexBase: get('dex-base'), dexCur: get('dex-cur'),
|
||
insBase: get('ins-base'), insCur: get('ins-cur'),
|
||
migBase: get('mig-base'), migCur: get('mig-cur'),
|
||
wlpBase: get('wlp-base'), wlpCur: get('wlp-cur'),
|
||
hpMax: get('hpMax'), hpCur: get('hpCur'),
|
||
mpMax: get('mpMax'), mpCur: get('mpCur'),
|
||
ipMax: get('ipMax'), ipCur: get('ipCur'),
|
||
fp: get('fpCount'),
|
||
backpack: get('backpack'),
|
||
accName: get('acc-name'), accDesc: get('acc-desc'),
|
||
armName: get('arm-name'), armDesc: get('arm-desc'),
|
||
mhName: get('mh-name'), mhDesc: get('mh-desc'),
|
||
ohName: get('oh-name'), ohDesc: get('oh-desc'),
|
||
heroicSkills: get('heroicSkills'),
|
||
ritualsNotes: get('ritualsNotes'),
|
||
statusesActive, martialChecked, disciplinesChecked,
|
||
bonds, primaryClasses, otherClasses, spells
|
||
};
|
||
}
|
||
|
||
function saveSheet() {
|
||
const data = collectData();
|
||
localStorage.setItem('fabulaUltimaSheet', JSON.stringify(data));
|
||
const st = document.getElementById('saveStatus');
|
||
st.classList.add('show');
|
||
setTimeout(() => st.classList.remove('show'), 2000);
|
||
}
|
||
|
||
function tryAutoLoad() {
|
||
const raw = localStorage.getItem('fabulaUltimaSheet');
|
||
if (raw) try { applyData(JSON.parse(raw)); } catch(e) {}
|
||
}
|
||
|
||
function loadSheet() {
|
||
const raw = localStorage.getItem('fabulaUltimaSheet');
|
||
if (!raw) return alert('No saved sheet found.');
|
||
try { applyData(JSON.parse(raw)); } catch(e) { alert('Could not load sheet.'); }
|
||
}
|
||
|
||
function applyData(d) {
|
||
const set = (id, val) => { const el = document.getElementById(id); if (el && val !== undefined) el.value = val; };
|
||
set('charName', d.name); set('charPronouns', d.pronouns);
|
||
set('charIdentity', d.identity); set('charTheme', d.theme); set('charOrigin', d.origin);
|
||
set('charTraits', d.traits);
|
||
level = d.level || 1;
|
||
document.getElementById('levelDisplay').textContent = level;
|
||
set('xpCurrent', d.xp); set('zenit', d.zenit);
|
||
set('initMod', d.initMod); set('defense', d.defense); set('magDef', d.magDef);
|
||
set('dex-base', d.dexBase); set('dex-cur', d.dexCur);
|
||
set('ins-base', d.insBase); set('ins-cur', d.insCur);
|
||
set('mig-base', d.migBase); set('mig-cur', d.migCur);
|
||
set('wlp-base', d.wlpBase); set('wlp-cur', d.wlpCur);
|
||
set('hpMax', d.hpMax); set('hpCur', d.hpCur);
|
||
set('mpMax', d.mpMax); set('mpCur', d.mpCur);
|
||
set('ipMax', d.ipMax); set('ipCur', d.ipCur);
|
||
set('fpCount', d.fp);
|
||
set('backpack', d.backpack);
|
||
set('acc-name', d.accName); set('acc-desc', d.accDesc);
|
||
set('arm-name', d.armName); set('arm-desc', d.armDesc);
|
||
set('mh-name', d.mhName); set('mh-desc', d.mhDesc);
|
||
set('oh-name', d.ohName); set('oh-desc', d.ohDesc);
|
||
set('heroicSkills', d.heroicSkills);
|
||
set('ritualsNotes', d.ritualsNotes);
|
||
|
||
// Statuses
|
||
document.querySelectorAll('.status-item').forEach(el => {
|
||
const active = (d.statusesActive||[]).includes(el.dataset.status);
|
||
el.classList.toggle('active-status', active);
|
||
el.querySelector('.status-check').textContent = active ? '✗' : '';
|
||
});
|
||
|
||
// Martial
|
||
document.querySelectorAll('.martial-item').forEach(el => {
|
||
const checked = (d.martialChecked||[]).includes(el.querySelector('span').textContent);
|
||
el.classList.toggle('checked', checked);
|
||
el.querySelector('.martial-box').textContent = checked ? '✓' : '';
|
||
});
|
||
|
||
// Disciplines
|
||
document.querySelectorAll('.disc-item').forEach(el => {
|
||
const checked = (d.disciplinesChecked||[]).includes(el.querySelector('span').textContent);
|
||
el.classList.toggle('checked', checked);
|
||
el.querySelector('.disc-box').textContent = checked ? '✓' : '';
|
||
});
|
||
|
||
if (d.bonds) { bonds = d.bonds; renderBonds(); }
|
||
if (d.primaryClasses) { primaryClasses = d.primaryClasses; renderPrimaryClasses(); }
|
||
if (d.otherClasses) { otherClasses = d.otherClasses; renderOtherClasses(); }
|
||
if (d.spells) { spells = d.spells; renderSpells(); }
|
||
|
||
renderFP();
|
||
updateXPBar();
|
||
updateCrisis();
|
||
}
|
||
|
||
// Auto-save every 30s
|
||
setInterval(saveSheet, 30000);
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|