pf2e-spellbook 1.0.0 β 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/bin/cli.mjs +86 -0
- package/dist/icon.svg +3 -3
- package/dist/index.html +568 -269
- package/dist/manifest.webmanifest +2 -2
- package/dist/sw.js +1 -1
- package/package.json +22 -4
- package/src/classes.js +74 -35
- package/src/engine.js +229 -67
- package/src/icon.svg +3 -3
- package/src/manifest.webmanifest +2 -2
- package/src/styles.css +229 -151
- package/src/template.html +36 -16
- package/tools/test.mjs +102 -5
package/src/template.html
CHANGED
|
@@ -3,12 +3,24 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
|
-
<meta name="theme-color" content="#
|
|
6
|
+
<meta name="theme-color" content="#14161b">
|
|
7
7
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
8
8
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
9
9
|
<meta name="apple-mobile-web-app-title" content="Spellbook">
|
|
10
10
|
<meta name="description" content="An interactive Pathfinder 2e spellbook: prepare, browse, and track your spells.">
|
|
11
|
-
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0
|
|
11
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%237b6cf0' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><path d='M12 6.5C9.5 4.7 6 4.7 3.5 6v13c2.5-1.3 6-1.3 8.5.5 2.5-1.8 6-1.8 8.5-.5V6c-2.5-1.3-6-1.3-8.5.5z'/><path d='M12 6.5v13'/></svg>">
|
|
12
|
+
<!-- Set the light/dark theme before first paint (avoids a flash); the engine reconciles fully on load. -->
|
|
13
|
+
<script>
|
|
14
|
+
(function(){try{
|
|
15
|
+
var lib=JSON.parse(localStorage.getItem("pf2eSpellbook.v3"))||{};
|
|
16
|
+
var s=lib.settings||{}, mode=s.themeMode||"auto";
|
|
17
|
+
if(mode!=="light"&&mode!=="dark") mode=(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)?"dark":"light";
|
|
18
|
+
var root=document.documentElement; root.dataset.theme=mode;
|
|
19
|
+
var c=s.custom;
|
|
20
|
+
if(c){ if(c.accent)root.style.setProperty("--accent",c.accent); if(c.bg)root.style.setProperty("--bg",c.bg);
|
|
21
|
+
if(c.ink)root.style.setProperty("--ink",c.ink); if(c.surface)root.style.setProperty("--surface",c.surface); }
|
|
22
|
+
}catch(e){}})();
|
|
23
|
+
</script>
|
|
12
24
|
<!-- PWA: present alongside index.html on the hosted (GitHub Pages) copy; harmlessly
|
|
13
25
|
absent when this file is downloaded and opened on its own. -->
|
|
14
26
|
<link rel="manifest" href="manifest.webmanifest">
|
|
@@ -22,8 +34,8 @@
|
|
|
22
34
|
|
|
23
35
|
<header class="top hide">
|
|
24
36
|
<div class="tophead">
|
|
25
|
-
<h1><span id="charIcon"
|
|
26
|
-
<button class="menubtn" onclick="openMenu()" title="Characters & backup" aria-label="Characters menu"
|
|
37
|
+
<h1><span id="charIcon"></span> <span id="charNameHead">Spellbook</span></h1>
|
|
38
|
+
<button class="menubtn" onclick="openMenu()" title="Characters & backup" aria-label="Characters menu"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="8" r="3.4"/><path d="M5.5 20a6.5 6.5 0 0 1 13 0"/></svg></button>
|
|
27
39
|
</div>
|
|
28
40
|
<div class="sub" id="charSub">Pathfinder 2e (Remaster)</div>
|
|
29
41
|
<div class="statbar" id="statbar">
|
|
@@ -38,11 +50,14 @@
|
|
|
38
50
|
|
|
39
51
|
<!-- ================= CHARACTER MENU ================= -->
|
|
40
52
|
<section id="view-menu" class="wrap hide">
|
|
41
|
-
<h2
|
|
53
|
+
<h2>Appearance</h2>
|
|
54
|
+
<div id="appearancePanel"></div>
|
|
55
|
+
|
|
56
|
+
<h2 style="margin-top:24px">Use offline</h2>
|
|
42
57
|
<p class="meta">Everything you do β your characters and prepared spells β is saved <b>on this device only</b>, in your browser. Nothing is sent anywhere. Keep a copy so it always works, even with no signal:</p>
|
|
43
58
|
<div class="row">
|
|
44
|
-
<button class="btn" onclick="downloadOffline()"
|
|
45
|
-
<button class="btn secondary" id="installBtn" style="display:none" onclick="installApp()"
|
|
59
|
+
<button class="btn" onclick="downloadOffline()">Save offline copy</button>
|
|
60
|
+
<button class="btn secondary" id="installBtn" style="display:none" onclick="installApp()">Install app</button>
|
|
46
61
|
</div>
|
|
47
62
|
<p class="meta" id="offlineHint" style="font-size:.8rem">βSave offline copyβ downloads this whole spellbook as a single file you can open any time β no internet needed. βInstall appβ adds it to your home screen like a normal app (appears when your browser supports it).</p>
|
|
48
63
|
|
|
@@ -54,10 +69,15 @@
|
|
|
54
69
|
<h2 style="margin-top:24px">Share & back up</h2>
|
|
55
70
|
<p class="meta">Export your <b>active</b> character to a code you can save or send to a friend. To load one, paste a code below and tap Import β it's added as a new character.</p>
|
|
56
71
|
<div class="row">
|
|
57
|
-
<button class="btn secondary" onclick="exportCharacter()"
|
|
58
|
-
<button class="btn secondary" onclick="importCharacter()"
|
|
72
|
+
<button class="btn secondary" onclick="exportCharacter()">Export active</button>
|
|
73
|
+
<button class="btn secondary" onclick="importCharacter()">Import code</button>
|
|
59
74
|
</div>
|
|
60
75
|
<textarea id="menuIO" rows="3" placeholder="Character code appears here on Export β or paste one here, then tap Import."></textarea>
|
|
76
|
+
|
|
77
|
+
<h2 style="margin-top:24px">Support</h2>
|
|
78
|
+
<p class="meta">This spellbook is a free fan project. If it has earned a place at your table, you can help keep it going:</p>
|
|
79
|
+
<a class="btn secondary kofi" href="https://ko-fi.com/whytevoyd" target="_blank" rel="noopener"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8h13v6.5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4z"/><path d="M16.5 9h1.75a2.75 2.75 0 1 1 0 5.5H16.5"/><path d="M10 15.2c-1.8-1.1-2.7-2.2-2.7-3.2 0-.9.7-1.6 1.6-1.6.5 0 .9.2 1.1.5.2-.3.6-.5 1.1-.5.9 0 1.6.7 1.6 1.6 0 1-.9 2.1-2.7 3.2z" fill="currentColor" stroke-width="0"/></svg> Support on Ko-fi</a>
|
|
80
|
+
|
|
61
81
|
<button class="btn secondary" onclick="closeMenu()">β Back</button>
|
|
62
82
|
<p class="meta" style="text-align:center;margin-top:18px;font-size:.8rem">Spell data: Paizo (OGL/ORC) via the Foundry VTT pf2e project Β· unofficial fan tool<br><span id="dataStamp"></span></p>
|
|
63
83
|
</section>
|
|
@@ -75,7 +95,7 @@
|
|
|
75
95
|
<!-- ================= PREPARE / REPERTOIRE ================= -->
|
|
76
96
|
<section id="view-prepare" class="wrap hide">
|
|
77
97
|
<p class="meta" id="prepClassLine"></p>
|
|
78
|
-
<div class="banner hide" id="prepPreviewBanner"
|
|
98
|
+
<div class="banner hide" id="prepPreviewBanner">Spell slots, cantrips, and focus spells are fully tracked. A few subclass extras (blood magic, oracular curse, companions/familiars) stay on your character sheet β noted below where relevant.</div>
|
|
79
99
|
|
|
80
100
|
<h2>β Your character</h2>
|
|
81
101
|
<p class="meta">Copy these from your character sheet.</p>
|
|
@@ -112,9 +132,9 @@
|
|
|
112
132
|
<!-- ================= BROWSE ================= -->
|
|
113
133
|
<section id="view-browse" class="wrap hide">
|
|
114
134
|
<h2 id="browseTitle">All spells</h2>
|
|
115
|
-
<input type="text" id="search" placeholder="
|
|
135
|
+
<input type="text" id="search" placeholder="Search spellsβ¦" oninput="renderBrowse()">
|
|
116
136
|
<div class="filterbar" id="rankChips"></div>
|
|
117
|
-
<button class="chip" id="filterToggle" onclick="toggleBrowseFilters()" aria-expanded="false"
|
|
137
|
+
<button class="chip" id="filterToggle" onclick="toggleBrowseFilters()" aria-expanded="false"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/><circle cx="9" cy="7" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="8" cy="17" r="2"/></svg> Filters</button>
|
|
118
138
|
<div id="browseFilters" class="hide"></div>
|
|
119
139
|
<div id="browseList"></div>
|
|
120
140
|
</section>
|
|
@@ -129,10 +149,10 @@
|
|
|
129
149
|
<div class="toast" id="toast"></div>
|
|
130
150
|
|
|
131
151
|
<nav class="bottom hide">
|
|
132
|
-
<button id="nav-today" class="on" onclick="go('today')"><span class="ic"
|
|
133
|
-
<button id="nav-prepare" onclick="go('prepare')"><span class="ic"
|
|
134
|
-
<button id="nav-browse" onclick="go('browse')"><span class="ic"
|
|
135
|
-
<button id="nav-help" onclick="go('help')"><span class="ic"
|
|
152
|
+
<button id="nav-today" class="on" onclick="go('today')"><span class="ic"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="7" y="3.5" width="12.5" height="17" rx="2"/><path d="M4.5 7v11.5a2 2 0 0 0 2 2h9.5"/></svg></span>Cast Today</button>
|
|
153
|
+
<button id="nav-prepare" onclick="go('prepare')"><span class="ic"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 18.5h18"/><path d="M6.5 18.5a5.5 5.5 0 0 1 11 0"/><path d="M12 3v3M4.2 8.7l1.7 1.7M19.8 8.7l-1.7 1.7"/></svg></span>Prepare</button>
|
|
154
|
+
<button id="nav-browse" onclick="go('browse')"><span class="ic"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5.2A2 2 0 0 1 6 3.2h5.2v15.6H6a2 2 0 0 0-2 2z"/><path d="M20 5.2a2 2 0 0 0-2-2h-5.2v15.6H18a2 2 0 0 1 2 2z"/></svg></span>All Spells</button>
|
|
155
|
+
<button id="nav-help" onclick="go('help')"><span class="ic"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M9.4 9.4a2.6 2.6 0 1 1 3.6 2.4c-.9.5-1.1 1-1.1 1.9"/><path d="M12 16.7v.01"/></svg></span>Help</button>
|
|
136
156
|
</nav>
|
|
137
157
|
|
|
138
158
|
<script>
|
package/tools/test.mjs
CHANGED
|
@@ -210,7 +210,9 @@ ok(ev("state.cast['focuspool']") === 1, "casting a focus spell spends a point");
|
|
|
210
210
|
w.castFocus(2); w.castFocus(2);
|
|
211
211
|
ok(ev("state.cast['focuspool']") === 2, "focus points cap at pool size");
|
|
212
212
|
w.refocus();
|
|
213
|
-
ok(ev("state.cast['focuspool']") ===
|
|
213
|
+
ok(ev("state.cast['focuspool']") === 1, "refocus recovers 1 focus point (RAW), not the whole pool");
|
|
214
|
+
w.refocus();
|
|
215
|
+
ok(ev("state.cast['focuspool']") === 0, "a second refocus recovers the next point");
|
|
214
216
|
|
|
215
217
|
console.log("\n# FOCUS CANTRIP: Bard composition is at-will");
|
|
216
218
|
w.chooseClass("bard");
|
|
@@ -308,6 +310,7 @@ ok(sumRepGroups.every(g => +g.dataset.reprank >= 3), "summoner L7 repertoire has
|
|
|
308
310
|
console.log("\n# PSYCHIC (occult spontaneous, 2 slots/rank, full prof)");
|
|
309
311
|
w.chooseClass("psychic");
|
|
310
312
|
ok(w.activeTradition() === "occult" && w.isSpontaneous(), "psychic = occult spontaneous");
|
|
313
|
+
ok(w.activeClass().cantrips === 3, "psychic knows 3 regular cantrips (the 3 psi cantrips live in the Focus section)");
|
|
311
314
|
const psySlots = { 1:"{\"1\":1}", 4:"{\"1\":2,\"2\":2}", 5:"{\"1\":2,\"2\":2,\"3\":1}" };
|
|
312
315
|
for (const [lvl, exp] of Object.entries(psySlots)) {
|
|
313
316
|
ok(JSON.stringify(w.classSlots(+lvl)) === exp, `psychic slots L${lvl} = ${exp} (got ${JSON.stringify(w.classSlots(+lvl))})`);
|
|
@@ -323,14 +326,75 @@ w.chooseClass("animist");
|
|
|
323
326
|
ok(!w.isSpontaneous() && w.activeClass().keyAbility === "Wisdom", "animist = prepared, Wisdom");
|
|
324
327
|
ok(w.activeTradition() === "divine", "animist casts the divine tradition");
|
|
325
328
|
ok(w.classSlots(1)[1] === 2 && w.classSlots(19)[10] === 1, "animist uses the full-caster slot table (2Γ rank 1 at L1; 10th rank at L19)");
|
|
329
|
+
ok(w.activeClass().cantrips === 4, "animist prepares 4 cantrips (2 chosen + 2 from apparitions)");
|
|
326
330
|
ev("state.level=19; state.keyMod=5;");
|
|
327
331
|
ok(w.spellDC() === 42, `animist L19 DC = 42 (legendary full caster; got ${w.spellDC()})`);
|
|
328
332
|
ok(w.hasFocus() && w.classFocusSpells().length > 0, `animist surfaces vessel/apparition focus spells (${w.classFocusSpells().length})`);
|
|
329
333
|
ev("state.level=5"); w.go("prepare");
|
|
330
334
|
ok(d.querySelectorAll(".slotSel").length > 0 && d.querySelectorAll(".repSel").length === 0, "animist prepares into slots, not a repertoire");
|
|
331
335
|
|
|
336
|
+
console.log("\n# PSYCHIC: Amp psi cantrips + Unleash Psyche + refocus recharge");
|
|
337
|
+
w.chooseClass("psychic"); ev("state.level=5; state.keyMod=4;");
|
|
338
|
+
w.go("prepare");
|
|
339
|
+
[...d.querySelectorAll(".cantripSel")].forEach(s=>{ const o=[...s.options].find(o=>o.value); if(o)s.value=o.value; });
|
|
340
|
+
[...d.querySelectorAll('[data-reprank]')].forEach(group=>{ const r=+group.dataset.reprank; const sel=group.querySelector(".repSel"); const o=[...sel.options].find(o=>o.value&&w.findSpell(o.value).rank<=r); if(o)sel.value=o.value; });
|
|
341
|
+
w.setFocusPool(2);
|
|
342
|
+
const psiChk = d.querySelector('.focusChk[value="imaginary-weapon"]');
|
|
343
|
+
ok(!!psiChk, "psi cantrip (Imaginary Weapon) available in the focus checklist");
|
|
344
|
+
psiChk.checked = true;
|
|
345
|
+
w.savePrep();
|
|
346
|
+
const pToday = d.getElementById("todayContent").innerHTML;
|
|
347
|
+
ok(/at will/.test(pToday) && /Amp \(1 pt\)/.test(pToday), "psi cantrip is at-will with an Amp (1 pt) button");
|
|
348
|
+
ok(/Unleash Psyche/.test(pToday), "Unleash Psyche resource shown on Cast Today");
|
|
349
|
+
w.castFocus(2);
|
|
350
|
+
ok(ev("state.cast['focuspool']") === 1, "Amping a psi cantrip spends a focus point");
|
|
351
|
+
w.doCast("resource:unleash", 1);
|
|
352
|
+
ok(ev("state.cast['resource:unleash']") === 1, "Unleash Psyche marks its daily use");
|
|
353
|
+
w.refocus();
|
|
354
|
+
ok(ev("state.cast['focuspool']") === 0, "refocus recovers the spent focus point");
|
|
355
|
+
ok((ev("state.cast['resource:unleash']")||0) === 0, "refocus also recharges Unleash Psyche (recharge:'refocus')");
|
|
356
|
+
|
|
357
|
+
console.log("\n# ORACLE: cursebound stage tracker");
|
|
358
|
+
w.chooseClass("oracle"); ev("state.level=5; state.keyMod=4;");
|
|
359
|
+
w.go("prepare");
|
|
360
|
+
[...d.querySelectorAll(".cantripSel")].forEach(s=>{ const o=[...s.options].find(o=>o.value); if(o)s.value=o.value; });
|
|
361
|
+
[...d.querySelectorAll('[data-reprank]')].forEach(group=>{ const r=+group.dataset.reprank; const sel=group.querySelector(".repSel"); const o=[...sel.options].find(o=>o.value&&w.findSpell(o.value).rank<=r); if(o)sel.value=o.value; });
|
|
362
|
+
w.savePrep();
|
|
363
|
+
ok(/Oracular curse/.test(d.getElementById("todayContent").innerHTML), "oracle Today shows the curse stage tracker");
|
|
364
|
+
w.stageUp("curse"); w.stageUp("curse");
|
|
365
|
+
ok(ev("state.cast['stage:curse']") === 2, "curse stage increments");
|
|
366
|
+
w.stageDown("curse");
|
|
367
|
+
ok(ev("state.cast['stage:curse']") === 1, "curse stage decrements");
|
|
368
|
+
w.stageUp("curse"); w.stageUp("curse"); w.stageUp("curse"); w.stageUp("curse");
|
|
369
|
+
ok(ev("state.cast['stage:curse']") === 4, "curse stage caps at its max (4)");
|
|
370
|
+
w.newDay();
|
|
371
|
+
ok((ev("state.cast['stage:curse']")||0) === 0, "resting clears the curse stage");
|
|
372
|
+
|
|
373
|
+
console.log("\n# MAGUS: Studious Spells (restricted utility slots, L7+)");
|
|
374
|
+
w.chooseClass("magus"); ev("state.level=5; state.keyMod=4;");
|
|
375
|
+
w.go("prepare");
|
|
376
|
+
ok(d.querySelectorAll(".studiousSel").length === 0, "no studious slots before level 7");
|
|
377
|
+
ev("state.level=7"); w.go("prepare");
|
|
378
|
+
const studSel = [...d.querySelectorAll(".studiousSel")];
|
|
379
|
+
ok(studSel.length === 2, "two studious slots at level 7");
|
|
380
|
+
const studOpts = [...studSel[0].options].filter(o=>o.value).map(o=>o.value).sort();
|
|
381
|
+
ok(JSON.stringify(studOpts) === JSON.stringify(["gecko-grip","sure-strike","water-breathing"]), "studious slots restricted to the utility list");
|
|
382
|
+
[...d.querySelectorAll(".cantripSel")].forEach(s=>{ const o=[...s.options].find(o=>o.value); if(o)s.value=o.value; });
|
|
383
|
+
[...d.querySelectorAll(".slotSel")].forEach(sel=>{ const r=+sel.dataset.rank; const o=[...sel.options].find(o=>o.value&&w.findSpell(o.value).rank<=r); if(o)sel.value=o.value; });
|
|
384
|
+
studSel[0].value="sure-strike"; studSel[1].value="water-breathing";
|
|
385
|
+
w.savePrep();
|
|
386
|
+
ok(JSON.stringify(ev("state.prepared.studious.studious")) === JSON.stringify(["sure-strike","water-breathing"]), "studious selections saved");
|
|
387
|
+
ok(/Studious spells/.test(d.getElementById("todayContent").innerHTML), "studious slots appear on Cast Today");
|
|
388
|
+
|
|
332
389
|
console.log("\n# class picker now lists all 11");
|
|
333
390
|
ok(ev("CLASS_ORDER.length") === 11, "class picker offers all 11 classes");
|
|
391
|
+
ok(ev("CLASS_ORDER.every(id => !CLASSES[id].preview)"), "no class remains in preview mode (all fully released)");
|
|
392
|
+
w.showClassPicker();
|
|
393
|
+
ok(!/previewtag">preview/.test(d.getElementById("view-classpick").innerHTML), "class picker renders no 'preview' tags");
|
|
394
|
+
{
|
|
395
|
+
w.chooseClass("magus"); w.go("prepare");
|
|
396
|
+
ok(d.getElementById("prepPreviewBanner").classList.contains("hide"), "Prepare view hides the preview banner for a released class");
|
|
397
|
+
}
|
|
334
398
|
|
|
335
399
|
console.log("\n# LEGACY / REMASTER compatibility");
|
|
336
400
|
// every spell has a legacy array; aliases resolve to real spells
|
|
@@ -359,7 +423,7 @@ ok(w.actionLabel("2") === "ββ 2 actions", "single action glyph");
|
|
|
359
423
|
ok(w.actionLabel("1 to 3").startsWith("βββββ"), "1-to-3 action range glyph");
|
|
360
424
|
ok(w.actionLabel("1 or 2").startsWith("ββββ"), "'1 or 2' renders as a glyph range");
|
|
361
425
|
ok(w.actionLabel("reaction") === "β€³ Reaction", "reaction glyph");
|
|
362
|
-
ok(w.actionLabel("10 minutes") === "
|
|
426
|
+
ok(w.actionLabel("10 minutes") === "10 minutes", "long casts show the plain time");
|
|
363
427
|
|
|
364
428
|
console.log("\n# in-app data integrity guard");
|
|
365
429
|
ok(ev("new Set(SPELLS.map(s=>s.slug)).size === SPELLS.length"), "all spell slugs unique (no findSpell collisions)");
|
|
@@ -388,7 +452,7 @@ w.chooseClass("cleric"); ev("state.level=3"); w.go("prepare");
|
|
|
388
452
|
[...d.querySelectorAll(".slotSel")].forEach(sel=>{ if(sel.dataset.rank==="2"){ sel.value="heal"; } else { const o=[...sel.options].find(o=>o.value); if(o) sel.value=o.value; } });
|
|
389
453
|
[...d.querySelectorAll(".cantripSel")].forEach(s=>{ const o=[...s.options].find(o=>o.value); if(o) s.value=o.value; });
|
|
390
454
|
w.savePrep();
|
|
391
|
-
ok(
|
|
455
|
+
ok(/dmgchip heal[^>]*>\s*2d8/.test(d.getElementById("todayContent").innerHTML), "Heal slotted at rank 2 shows a 2d8 healing chip on Cast Today");
|
|
392
456
|
|
|
393
457
|
|
|
394
458
|
console.log("\n# SUSTAINED TRACKER (#2)");
|
|
@@ -399,7 +463,7 @@ w.go("prepare");
|
|
|
399
463
|
[...d.querySelectorAll(".slotSel")].forEach(sel=>{ const r=+sel.dataset.rank; if(r===sus.rank){ sel.value=sus.slug; } else { const o=[...sel.options].find(o=>o.value); if(o) sel.value=o.value; } });
|
|
400
464
|
[...d.querySelectorAll(".cantripSel")].forEach(x=>{ const o=[...x.options].find(o=>o.value); if(o) x.value=o.value; });
|
|
401
465
|
w.savePrep();
|
|
402
|
-
ok(
|
|
466
|
+
ok(/susbtn[^>]*>Sustain</.test(d.getElementById("todayContent").innerHTML), "sustained spell shows a Sustain button on Cast Today");
|
|
403
467
|
w.startSustain(sus.slug);
|
|
404
468
|
ok((ev("state.sustaining")||[]).includes(sus.slug), "startSustain adds the spell to the tracker");
|
|
405
469
|
ok(/Sustaining now/.test(d.getElementById("todayContent").innerHTML), "Sustaining-now bar appears");
|
|
@@ -480,7 +544,7 @@ ok([...d.querySelectorAll("#focusChecklist .fcheck")].every(el=>!el.classList.co
|
|
|
480
544
|
|
|
481
545
|
console.log("\n# RITUALS BROWSER + INSTALL (#10)");
|
|
482
546
|
w.chooseClass("wizard"); w.go("browse");
|
|
483
|
-
ok(
|
|
547
|
+
ok(/Rituals/.test(d.getElementById("rankChips").innerHTML), "Rituals chip present in Browse");
|
|
484
548
|
w.setBrowseRank("rituals");
|
|
485
549
|
d.getElementById("search").value=""; w.setBrowseSave("all"); w.setBrowseAction("all"); w.setBrowseTrait(""); w.renderBrowse();
|
|
486
550
|
ok(d.getElementById("browseTitle").textContent.includes("Ritual"), "browse titled Rituals");
|
|
@@ -532,5 +596,38 @@ ok(/on this device only/i.test(d.getElementById("view-menu").innerHTML), "menu r
|
|
|
532
596
|
ok(alerted, "installApp() explains manual install when no prompt is available");
|
|
533
597
|
}
|
|
534
598
|
|
|
599
|
+
console.log("\n# THEME (light/dark + per-class accent + custom palette)");
|
|
600
|
+
ok(typeof w.applyTheme === "function", "applyTheme() present");
|
|
601
|
+
w.chooseClass("druid");
|
|
602
|
+
const root = d.documentElement;
|
|
603
|
+
ok(root.dataset.theme === "light" || root.dataset.theme === "dark", "a data-theme mode is set");
|
|
604
|
+
const druidAccent = root.style.getPropertyValue("--accent").trim().toLowerCase();
|
|
605
|
+
ok(druidAccent === ev("CLASSES.druid.color").toLowerCase(), `accent matches the class colour (druid ${druidAccent})`);
|
|
606
|
+
w.setThemeMode("light");
|
|
607
|
+
ok(root.dataset.theme === "light", "setThemeMode('light') switches to light");
|
|
608
|
+
w.setThemeMode("dark");
|
|
609
|
+
ok(root.dataset.theme === "dark", "setThemeMode('dark') switches to dark");
|
|
610
|
+
w.setThemeMode("auto");
|
|
611
|
+
w.setCustomColor("accent", "#123456");
|
|
612
|
+
ok(root.style.getPropertyValue("--accent").trim() === "#123456", "custom accent overrides the class colour");
|
|
613
|
+
ok(ev("library.settings.custom.accent") === "#123456", "custom colour saved to settings");
|
|
614
|
+
{
|
|
615
|
+
const saved = JSON.parse(w.localStorage.getItem("pf2eSpellbook.v3"));
|
|
616
|
+
ok(saved.settings && saved.settings.custom && saved.settings.custom.accent === "#123456", "settings persist to localStorage");
|
|
617
|
+
}
|
|
618
|
+
w.resetTheme();
|
|
619
|
+
ok(ev("library.settings.custom") === null, "resetTheme clears the custom palette");
|
|
620
|
+
ok(root.style.getPropertyValue("--accent").trim().toLowerCase() === ev("CLASSES.druid.color").toLowerCase(), "reset restores the class default accent");
|
|
621
|
+
w.openMenu();
|
|
622
|
+
const ap = d.getElementById("appearancePanel");
|
|
623
|
+
ok(!!ap && /Reset to class default/.test(ap.innerHTML), "Appearance panel renders in the menu");
|
|
624
|
+
ok(ap.querySelectorAll('input[type="color"]').length === 4, "four custom-colour pickers present");
|
|
625
|
+
|
|
626
|
+
console.log("\n# NO EMOJI (class/nav/section markers are SVG or text)");
|
|
627
|
+
const EMOJI = /\p{Emoji_Presentation}/u;
|
|
628
|
+
ok(!EMOJI.test(html), "built index.html contains no emoji");
|
|
629
|
+
ok(/class="icn"/.test(html), "inline SVG line icons are used");
|
|
630
|
+
ok(ev('typeof iconSvg') === "function" && /<svg/.test(w.iconSvg("cleric")), "iconSvg() renders an inline SVG");
|
|
631
|
+
|
|
535
632
|
console.log(`\n# RESULT: ${pass} passed, ${fail} failed`);
|
|
536
633
|
process.exit(fail ? 1 : 0);
|