chore: Format code with Prettier

This commit is contained in:
2026-06-05 02:41:10 +00:00
parent a9dc508940
commit 41bbb7a44b
5 changed files with 1031 additions and 609 deletions

View File

@@ -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;

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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
View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"prettier": "^3.8.3"
}
}