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/dist/index.html
CHANGED
|
@@ -3,168 +3,245 @@
|
|
|
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">
|
|
15
27
|
<link rel="apple-touch-icon" href="icon.svg">
|
|
16
28
|
<title>PF2e Spellbook</title>
|
|
17
29
|
<style>
|
|
30
|
+
/* ============================================================
|
|
31
|
+
THEME TOKENS
|
|
32
|
+
Base tokens flip with [data-theme]; --accent (+ --accent-ink) are
|
|
33
|
+
set by JS per class or per custom palette. Accent shades derive via
|
|
34
|
+
color-mix(), so one hue recolours the whole UI.
|
|
35
|
+
============================================================ */
|
|
18
36
|
:root{
|
|
19
|
-
--
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
--
|
|
28
|
-
--
|
|
29
|
-
--
|
|
37
|
+
/* accent — JS overrides --accent and --accent-ink on <html> */
|
|
38
|
+
--accent:#6c7a89;
|
|
39
|
+
--accent-ink:#ffffff;
|
|
40
|
+
--accent-dim:color-mix(in srgb, var(--accent) 55%, var(--line));
|
|
41
|
+
--accent-soft:color-mix(in srgb, var(--accent) 15%, transparent);
|
|
42
|
+
--accent-text:var(--accent);
|
|
43
|
+
|
|
44
|
+
/* fixed rank spectrum (left border bars), shared by both modes */
|
|
45
|
+
--rank0:#b58fd6; --rank1:#7fb0d6; --rank2:#6f9c8f; --rank3:#d6a86f;
|
|
46
|
+
--rank4:#d68f8f; --rank5:#9cc46a; --rank6:#c9c05a; --rank7:#6fb6d6;
|
|
47
|
+
--rank8:#c98fd6; --rank9:#d6a0c0; --rank10:var(--accent);
|
|
48
|
+
|
|
30
49
|
--radius:16px;
|
|
31
|
-
--tap:#3c2c1b;
|
|
32
50
|
}
|
|
51
|
+
|
|
52
|
+
/* ---- Dark (default) ---- */
|
|
53
|
+
:root, :root[data-theme="dark"]{
|
|
54
|
+
color-scheme:dark;
|
|
55
|
+
--bg:#14161b;
|
|
56
|
+
--surface:#1d2027;
|
|
57
|
+
--surface-2:#262a33;
|
|
58
|
+
--ink:#e8eaf0;
|
|
59
|
+
--muted:#9aa1b0;
|
|
60
|
+
--line:#2f3440;
|
|
61
|
+
--heal:#5bbf7e;
|
|
62
|
+
--harm:#d9637a;
|
|
63
|
+
--info:#6fa8dc;
|
|
64
|
+
--info-bg:color-mix(in srgb, var(--info) 22%, var(--surface));
|
|
65
|
+
--info-ink:#d6e8fb;
|
|
66
|
+
--accent-text:var(--accent);
|
|
67
|
+
--shadow:0 1px 2px rgba(0,0,0,.4), 0 4px 14px rgba(0,0,0,.25);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* ---- Light ---- */
|
|
71
|
+
:root[data-theme="light"]{
|
|
72
|
+
color-scheme:light;
|
|
73
|
+
--bg:#f5f6f8;
|
|
74
|
+
--surface:#ffffff;
|
|
75
|
+
--surface-2:#eef0f4;
|
|
76
|
+
--ink:#1b1e24;
|
|
77
|
+
--muted:#5d6470;
|
|
78
|
+
--line:#dfe3ea;
|
|
79
|
+
--heal:#2f9e5b;
|
|
80
|
+
--harm:#c43a57;
|
|
81
|
+
--info:#2b6cb0;
|
|
82
|
+
--info-bg:color-mix(in srgb, var(--info) 12%, var(--surface));
|
|
83
|
+
--info-ink:#0b3a68;
|
|
84
|
+
--accent-text:color-mix(in srgb, var(--accent) 72%, #000);
|
|
85
|
+
--shadow:0 1px 2px rgba(20,30,50,.08), 0 4px 14px rgba(20,30,50,.06);
|
|
86
|
+
}
|
|
87
|
+
|
|
33
88
|
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
|
|
34
89
|
html,body{margin:0;padding:0;}
|
|
90
|
+
html{background:var(--bg);}
|
|
35
91
|
body{
|
|
36
92
|
background:var(--bg);
|
|
37
93
|
color:var(--ink);
|
|
38
94
|
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
|
39
|
-
font-size:
|
|
95
|
+
font-size:18px;
|
|
40
96
|
line-height:1.5;
|
|
41
97
|
padding-bottom:84px; /* room for nav */
|
|
42
98
|
-webkit-text-size-adjust:100%;
|
|
43
99
|
}
|
|
44
|
-
h1,h2,h3{margin:0 0 .4em;line-height:1.2;}
|
|
45
|
-
h1{font-size:1.5rem;color:var(--
|
|
46
|
-
h2{font-size:1.
|
|
100
|
+
h1,h2,h3{margin:0 0 .4em;line-height:1.2;letter-spacing:-.01em;}
|
|
101
|
+
h1{font-size:1.5rem;color:var(--ink);font-weight:800;}
|
|
102
|
+
h2{font-size:1.2rem;color:var(--ink);font-weight:700;}
|
|
47
103
|
p{margin:.4em 0;}
|
|
48
104
|
.wrap{max-width:560px;margin:0 auto;padding:16px 14px;}
|
|
49
105
|
.hide{display:none !important;}
|
|
50
106
|
|
|
107
|
+
/* smooth theme + interaction transitions */
|
|
108
|
+
button,.chip,.card,.cast-card,.classcard,.browse-row,input,select,textarea,
|
|
109
|
+
.stat,nav.bottom button,details.help{
|
|
110
|
+
transition:background-color .15s ease, border-color .15s ease, color .15s ease;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* inline SVG icons inherit accent via currentColor */
|
|
114
|
+
.icn{display:inline-block;width:1em;height:1em;flex:0 0 auto;vertical-align:-.14em;}
|
|
115
|
+
|
|
51
116
|
/* ---- Header ---- */
|
|
52
117
|
header.top{
|
|
53
|
-
background:linear-gradient(180deg
|
|
54
|
-
border-bottom:
|
|
118
|
+
background:linear-gradient(180deg, color-mix(in srgb, var(--accent) 10%, var(--surface)), var(--surface));
|
|
119
|
+
border-bottom:1px solid var(--line);
|
|
55
120
|
padding:14px 16px;
|
|
56
121
|
position:sticky;top:0;z-index:20;
|
|
57
122
|
}
|
|
58
|
-
header.top h1{margin:0;font-size:1.
|
|
59
|
-
header.top
|
|
60
|
-
.
|
|
123
|
+
header.top h1{margin:0;font-size:1.3rem;display:flex;align-items:center;gap:10px;}
|
|
124
|
+
header.top h1 #charIcon{display:inline-flex;color:var(--accent-text);font-size:1.5rem;}
|
|
125
|
+
header.top .sub{color:var(--muted);font-size:.88rem;margin-top:2px;}
|
|
126
|
+
.statbar{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;}
|
|
61
127
|
.stat{
|
|
62
|
-
background:var(--
|
|
63
|
-
border-radius:12px;padding:
|
|
128
|
+
background:var(--surface-2);border:1px solid var(--line);
|
|
129
|
+
border-radius:12px;padding:8px 12px;flex:1;min-width:90px;text-align:center;
|
|
64
130
|
}
|
|
65
|
-
.stat .lbl{font-size:.
|
|
66
|
-
.stat .val{font-size:1.4rem;font-weight:
|
|
131
|
+
.stat .lbl{font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.6px;}
|
|
132
|
+
.stat .val{font-size:1.4rem;font-weight:800;color:var(--accent-text);}
|
|
67
133
|
|
|
68
134
|
/* ---- Cards ---- */
|
|
69
135
|
.card{
|
|
70
|
-
background:var(--
|
|
136
|
+
background:var(--surface);
|
|
71
137
|
border:1px solid var(--line);
|
|
72
138
|
border-radius:var(--radius);
|
|
73
139
|
padding:16px;margin:14px 0;
|
|
74
|
-
box-shadow:
|
|
75
|
-
}
|
|
76
|
-
.card.rank0{border-left:
|
|
77
|
-
.card.rank1{border-left:
|
|
78
|
-
.card.rank2{border-left:
|
|
79
|
-
.card.rank3{border-left:
|
|
80
|
-
.card.rank4{border-left:
|
|
81
|
-
.card.rank5{border-left:
|
|
82
|
-
.card.rank6{border-left:
|
|
83
|
-
.card.rank7{border-left:
|
|
84
|
-
.card.rank8{border-left:
|
|
85
|
-
.card.rank9{border-left:
|
|
86
|
-
.card.rank10{border-left:
|
|
87
|
-
.card.heal{border-left:
|
|
88
|
-
.card.harm{border-left:
|
|
140
|
+
box-shadow:var(--shadow);
|
|
141
|
+
}
|
|
142
|
+
.card.rank0{border-left:5px solid var(--rank0);}
|
|
143
|
+
.card.rank1{border-left:5px solid var(--rank1);}
|
|
144
|
+
.card.rank2{border-left:5px solid var(--rank2);}
|
|
145
|
+
.card.rank3{border-left:5px solid var(--rank3);}
|
|
146
|
+
.card.rank4{border-left:5px solid var(--rank4);}
|
|
147
|
+
.card.rank5{border-left:5px solid var(--rank5);}
|
|
148
|
+
.card.rank6{border-left:5px solid var(--rank6);}
|
|
149
|
+
.card.rank7{border-left:5px solid var(--rank7);}
|
|
150
|
+
.card.rank8{border-left:5px solid var(--rank8);}
|
|
151
|
+
.card.rank9{border-left:5px solid var(--rank9);}
|
|
152
|
+
.card.rank10{border-left:5px solid var(--rank10);}
|
|
153
|
+
.card.heal{border-left:5px solid var(--heal);}
|
|
154
|
+
.card.harm{border-left:5px solid var(--harm);}
|
|
89
155
|
|
|
90
156
|
.spell-head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;}
|
|
91
|
-
.spell-name{font-size:1.
|
|
157
|
+
.spell-name{font-size:1.25rem;font-weight:700;color:var(--ink);margin:0;}
|
|
92
158
|
.rankpill{
|
|
93
|
-
background:var(--
|
|
94
|
-
border-radius:20px;padding:3px 12px;font-size:.
|
|
159
|
+
background:var(--surface-2);border:1px solid var(--accent-dim);color:var(--accent-text);
|
|
160
|
+
border-radius:20px;padding:3px 12px;font-size:.82rem;font-weight:700;white-space:nowrap;
|
|
95
161
|
}
|
|
96
162
|
.actions{
|
|
97
163
|
display:inline-flex;align-items:center;gap:6px;
|
|
98
|
-
background
|
|
164
|
+
background:var(--info-bg);border:1px solid var(--info);color:var(--info-ink);
|
|
99
165
|
border-radius:8px;padding:2px 10px;font-size:.85rem;font-weight:700;margin:8px 8px 4px 0;
|
|
100
166
|
}
|
|
101
167
|
.save-tag{
|
|
102
|
-
display:inline-block;
|
|
168
|
+
display:inline-block;
|
|
169
|
+
background:color-mix(in srgb, var(--harm) 15%, var(--surface));
|
|
170
|
+
border:1px solid color-mix(in srgb, var(--harm) 45%, var(--line));
|
|
171
|
+
color:var(--harm);
|
|
103
172
|
border-radius:8px;padding:2px 10px;font-size:.85rem;font-weight:700;margin:8px 8px 4px 0;
|
|
104
173
|
}
|
|
105
|
-
.meta{font-size:.
|
|
174
|
+
.meta{font-size:.9rem;color:var(--muted);margin:6px 0;}
|
|
106
175
|
.meta b{color:var(--ink);font-weight:600;}
|
|
107
176
|
.traits{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0;}
|
|
108
177
|
.trait{
|
|
109
|
-
background
|
|
110
|
-
font-size:.
|
|
178
|
+
background:var(--surface-2);border:1px solid var(--line);color:var(--muted);
|
|
179
|
+
font-size:.72rem;text-transform:uppercase;letter-spacing:.4px;
|
|
111
180
|
padding:2px 8px;border-radius:6px;
|
|
112
181
|
}
|
|
113
|
-
.desc{font-size:
|
|
182
|
+
.desc{font-size:1rem;line-height:1.55;margin-top:8px;}
|
|
114
183
|
.heighten{
|
|
115
|
-
margin-top:10px;padding:10px 12px;background:var(--
|
|
116
|
-
border-radius:10px;font-size:.
|
|
184
|
+
margin-top:10px;padding:10px 12px;background:var(--surface-2);
|
|
185
|
+
border-radius:10px;font-size:.92rem;color:var(--muted);
|
|
117
186
|
}
|
|
118
|
-
.heighten b{color:var(--
|
|
187
|
+
.heighten b{color:var(--accent-text);}
|
|
119
188
|
|
|
120
189
|
/* ---- Buttons / inputs ---- */
|
|
121
190
|
button{font-family:inherit;font-size:1.05rem;cursor:pointer;}
|
|
122
191
|
.btn{
|
|
123
|
-
display:block;width:100%;padding:
|
|
124
|
-
background:var(--
|
|
125
|
-
margin:14px 0;letter-spacing:.
|
|
192
|
+
display:block;width:100%;padding:15px;border-radius:12px;border:none;
|
|
193
|
+
background:var(--accent);color:var(--accent-ink);font-weight:800;font-size:1.1rem;
|
|
194
|
+
margin:14px 0;letter-spacing:.2px;box-shadow:var(--shadow);
|
|
126
195
|
}
|
|
127
|
-
.btn.secondary{background:var(--
|
|
196
|
+
.btn.secondary{background:var(--surface-2);color:var(--accent-text);border:1px solid var(--accent-dim);box-shadow:none;}
|
|
128
197
|
.btn:active{transform:translateY(1px);}
|
|
129
|
-
.
|
|
198
|
+
a.btn{display:flex;align-items:center;justify-content:center;gap:8px;text-decoration:none;text-align:center;}
|
|
199
|
+
a.btn .icn{width:1.15em;height:1.15em;}
|
|
200
|
+
.linklike{background:none;border:none;color:var(--info);font-weight:700;text-decoration:underline;padding:0;font-size:inherit;}
|
|
130
201
|
|
|
131
202
|
label.field{display:block;margin:14px 0;}
|
|
132
|
-
label.field .name{display:block;font-weight:700;margin-bottom:6px;font-size:
|
|
203
|
+
label.field .name{display:block;font-weight:700;margin-bottom:6px;font-size:1rem;}
|
|
133
204
|
label.field .hint{display:block;color:var(--muted);font-size:.82rem;font-weight:400;margin-bottom:6px;}
|
|
205
|
+
input,select,textarea{accent-color:var(--accent);}
|
|
134
206
|
input,select{
|
|
135
|
-
width:100%;padding:
|
|
136
|
-
background:var(--
|
|
207
|
+
width:100%;padding:13px;font-size:1.05rem;border-radius:12px;
|
|
208
|
+
background:var(--surface-2);color:var(--ink);border:1px solid var(--line);
|
|
137
209
|
appearance:none;-webkit-appearance:none;
|
|
138
210
|
}
|
|
139
|
-
select{
|
|
211
|
+
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);}
|
|
212
|
+
select{background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);
|
|
140
213
|
background-position:calc(100% - 20px) center,calc(100% - 14px) center;
|
|
141
214
|
background-size:6px 6px,6px 6px;background-repeat:no-repeat;padding-right:40px;}
|
|
215
|
+
input[type="color"]{padding:4px;height:44px;cursor:pointer;}
|
|
142
216
|
.row{display:flex;gap:10px;}
|
|
143
217
|
.row > *{flex:1;}
|
|
144
218
|
|
|
145
219
|
.seg{display:flex;gap:8px;margin:6px 0;}
|
|
146
220
|
.seg button{
|
|
147
|
-
flex:1;padding:
|
|
148
|
-
background:var(--
|
|
221
|
+
flex:1;padding:13px;border-radius:12px;border:1px solid var(--line);
|
|
222
|
+
background:var(--surface-2);color:var(--ink);font-weight:700;
|
|
223
|
+
display:inline-flex;align-items:center;justify-content:center;gap:8px;
|
|
149
224
|
}
|
|
150
|
-
.seg button.on
|
|
151
|
-
.seg button.on.
|
|
225
|
+
.seg button.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
|
|
226
|
+
.seg button.on.heal{background:var(--heal);color:#08140d;border-color:var(--heal);}
|
|
227
|
+
.seg button.on.harm{background:var(--harm);color:#1c0709;border-color:var(--harm);}
|
|
152
228
|
|
|
153
229
|
/* ---- Slot picker rows ---- */
|
|
154
230
|
.slotgroup{margin:18px 0;}
|
|
155
|
-
.slotgroup h3{color:var(--
|
|
231
|
+
.slotgroup h3{color:var(--accent-text);font-size:1.05rem;border-bottom:1px solid var(--line);padding-bottom:6px;display:flex;align-items:center;gap:8px;}
|
|
156
232
|
.slotrow{display:flex;align-items:center;gap:10px;margin:10px 0;}
|
|
157
233
|
.slotrow .num{
|
|
158
234
|
width:34px;height:34px;flex:0 0 34px;border-radius:50%;
|
|
159
|
-
background:var(--
|
|
235
|
+
background:var(--surface-2);border:1px solid var(--accent-dim);color:var(--accent-text);
|
|
160
236
|
display:flex;align-items:center;justify-content:center;font-weight:800;
|
|
161
237
|
}
|
|
238
|
+
.slotrow .num .icn{width:1.1rem;height:1.1rem;}
|
|
162
239
|
.slotrow select{flex:1;}
|
|
163
240
|
.prep-slot{margin:10px 0;}
|
|
164
241
|
.prep-slot .slotrow{margin:0;}
|
|
165
242
|
.prep-preview{margin:6px 0 0 44px;}
|
|
166
243
|
.prep-preview-inner{
|
|
167
|
-
background:var(--
|
|
244
|
+
background:var(--surface-2);border:1px solid var(--line);border-radius:10px;
|
|
168
245
|
padding:10px 12px;
|
|
169
246
|
}
|
|
170
247
|
.prep-preview .meta{font-size:.85rem;margin:2px 0;}
|
|
@@ -173,123 +250,128 @@ select{background-image:linear-gradient(45deg,transparent 50%,var(--gold) 50%),l
|
|
|
173
250
|
|
|
174
251
|
/* ---- Today cast cards ---- */
|
|
175
252
|
.cast-card{
|
|
176
|
-
background:var(--
|
|
177
|
-
padding:14px;margin:12px 0;border-left:
|
|
178
|
-
}
|
|
179
|
-
.cast-card.rank0{border-left-color
|
|
180
|
-
.cast-card.rank1{border-left-color
|
|
181
|
-
.cast-card.rank2{border-left-color
|
|
182
|
-
.cast-card.rank3{border-left-color
|
|
183
|
-
.cast-card.rank4{border-left-color
|
|
184
|
-
.cast-card.rank5{border-left-color
|
|
185
|
-
.cast-card.rank6{border-left-color
|
|
186
|
-
.cast-card.rank7{border-left-color
|
|
187
|
-
.cast-card.rank8{border-left-color
|
|
188
|
-
.cast-card.rank9{border-left-color
|
|
189
|
-
.cast-card.rank10{border-left-color:var(--
|
|
253
|
+
background:var(--surface);border:1px solid var(--line);border-radius:14px;
|
|
254
|
+
padding:14px;margin:12px 0;border-left:5px solid var(--line);box-shadow:var(--shadow);
|
|
255
|
+
}
|
|
256
|
+
.cast-card.rank0{border-left-color:var(--rank0);}
|
|
257
|
+
.cast-card.rank1{border-left-color:var(--rank1);}
|
|
258
|
+
.cast-card.rank2{border-left-color:var(--rank2);}
|
|
259
|
+
.cast-card.rank3{border-left-color:var(--rank3);}
|
|
260
|
+
.cast-card.rank4{border-left-color:var(--rank4);}
|
|
261
|
+
.cast-card.rank5{border-left-color:var(--rank5);}
|
|
262
|
+
.cast-card.rank6{border-left-color:var(--rank6);}
|
|
263
|
+
.cast-card.rank7{border-left-color:var(--rank7);}
|
|
264
|
+
.cast-card.rank8{border-left-color:var(--rank8);}
|
|
265
|
+
.cast-card.rank9{border-left-color:var(--rank9);}
|
|
266
|
+
.cast-card.rank10{border-left-color:var(--rank10);}
|
|
190
267
|
.cast-card.heal{border-left-color:var(--heal);}
|
|
191
268
|
.cast-card.harm{border-left-color:var(--harm);}
|
|
192
|
-
.cast-card.spent{opacity:.
|
|
269
|
+
.cast-card.spent{opacity:.45;}
|
|
193
270
|
.cast-top{display:flex;justify-content:space-between;align-items:center;gap:10px;}
|
|
194
|
-
.cast-top .nm{font-size:1.
|
|
271
|
+
.cast-top .nm{font-size:1.15rem;font-weight:700;}
|
|
195
272
|
.uses{display:flex;gap:6px;align-items:center;}
|
|
196
|
-
.pip{width:
|
|
197
|
-
.pip.full{background:var(--
|
|
273
|
+
.pip{width:24px;height:24px;border-radius:50%;border:2px solid var(--accent);display:inline-block;}
|
|
274
|
+
.pip.full{background:var(--accent);}
|
|
198
275
|
.pip.spent{background:transparent;opacity:.35;}
|
|
199
276
|
.castbtn{
|
|
200
|
-
padding:10px 16px;border-radius:10px;border:none;background:var(--
|
|
201
|
-
color
|
|
277
|
+
padding:10px 16px;border-radius:10px;border:none;background:var(--accent);
|
|
278
|
+
color:var(--accent-ink);font-weight:800;font-size:1rem;
|
|
202
279
|
}
|
|
203
|
-
.castbtn.zero{background:var(--
|
|
280
|
+
.castbtn.zero{background:var(--surface-2);color:var(--muted);}
|
|
204
281
|
.unlim{font-size:.85rem;color:var(--heal);font-weight:700;}
|
|
205
282
|
.detailsbtn{
|
|
206
|
-
background:none;border:none;color:var(--
|
|
283
|
+
background:none;border:none;color:var(--info);font-weight:700;
|
|
207
284
|
padding:8px 0 0;font-size:.95rem;text-decoration:underline;
|
|
208
285
|
}
|
|
209
286
|
|
|
210
287
|
.susbtn{
|
|
211
|
-
padding:10px 14px;border-radius:10px;border:1px solid var(--
|
|
212
|
-
background
|
|
288
|
+
padding:10px 14px;border-radius:10px;border:1px solid var(--info);
|
|
289
|
+
background:var(--info-bg);color:var(--info-ink);font-weight:800;font-size:.95rem;
|
|
213
290
|
}
|
|
214
|
-
.susbtn.on{background:var(--
|
|
291
|
+
.susbtn.on{background:var(--info);color:var(--surface);}
|
|
215
292
|
.susrow{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:6px 0;border-top:1px solid var(--line);}
|
|
216
293
|
.susrow:first-of-type{border-top:none;}
|
|
217
294
|
.susrow span{font-weight:700;}
|
|
218
295
|
|
|
219
296
|
.dmgline{margin:4px 0 2px;}
|
|
220
297
|
.dmgchip{
|
|
221
|
-
display:inline-block;background
|
|
222
|
-
color:var(--
|
|
298
|
+
display:inline-block;background:var(--surface-2);border:1px solid var(--line);
|
|
299
|
+
color:var(--ink);font-weight:800;font-size:.92rem;border-radius:8px;padding:2px 10px;
|
|
223
300
|
}
|
|
301
|
+
.dmgchip.dmg{background:color-mix(in srgb, var(--harm) 15%, var(--surface));border-color:color-mix(in srgb, var(--harm) 40%, var(--line));color:var(--harm);}
|
|
302
|
+
.dmgchip.heal{background:color-mix(in srgb, var(--heal) 15%, var(--surface));border-color:color-mix(in srgb, var(--heal) 40%, var(--line));color:var(--heal);}
|
|
224
303
|
|
|
225
304
|
.empty{text-align:center;color:var(--muted);padding:40px 20px;}
|
|
226
305
|
|
|
227
306
|
.filterbar{display:flex;gap:8px;margin:6px 0 14px;flex-wrap:wrap;}
|
|
228
307
|
.chip{
|
|
229
|
-
padding:8px 14px;border-radius:20px;border:1px solid var(--
|
|
230
|
-
background:var(--
|
|
308
|
+
padding:8px 14px;border-radius:20px;border:1px solid var(--line);
|
|
309
|
+
background:var(--surface-2);color:var(--muted);font-weight:700;font-size:.88rem;
|
|
310
|
+
display:inline-flex;align-items:center;gap:6px;
|
|
231
311
|
}
|
|
232
|
-
.chip.on{background:var(--
|
|
312
|
+
.chip.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
|
|
233
313
|
#search{margin-bottom:8px;}
|
|
234
314
|
#filterToggle{margin:0 0 8px;}
|
|
235
|
-
#browseFilters{background:var(--
|
|
315
|
+
#browseFilters{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin:0 0 12px;}
|
|
236
316
|
.fgroup{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:4px 0;}
|
|
237
317
|
.fgroup .flabel{font-size:.8rem;color:var(--muted);font-weight:700;min-width:58px;}
|
|
238
318
|
|
|
239
319
|
.sectionhead{
|
|
240
|
-
color:var(--
|
|
320
|
+
color:var(--accent-text);font-size:1.1rem;font-weight:800;margin:22px 0 4px;
|
|
241
321
|
display:flex;align-items:center;gap:10px;
|
|
242
322
|
}
|
|
243
|
-
.sectionhead .count{font-size:.
|
|
323
|
+
.sectionhead .count{font-size:.82rem;color:var(--muted);font-weight:600;}
|
|
244
324
|
.divider{height:1px;background:var(--line);margin:4px 0 6px;}
|
|
245
325
|
|
|
246
326
|
.banner{
|
|
247
|
-
background
|
|
248
|
-
padding:12px 14px;margin:12px 0;color:var(--
|
|
327
|
+
background:var(--accent-soft);border:1px solid var(--accent-dim);border-radius:12px;
|
|
328
|
+
padding:12px 14px;margin:12px 0;color:var(--ink);font-size:.9rem;
|
|
249
329
|
}
|
|
250
|
-
.banner b{color:var(--
|
|
330
|
+
.banner b{color:var(--accent-text);}
|
|
251
331
|
|
|
252
|
-
details.help{background:var(--
|
|
253
|
-
details.help summary{padding:16px 0;font-weight:700;font-size:1.
|
|
332
|
+
details.help{background:var(--surface);border:1px solid var(--line);border-radius:14px;margin:12px 0;padding:0 16px;}
|
|
333
|
+
details.help summary{padding:16px 0;font-weight:700;font-size:1.05rem;color:var(--accent-text);cursor:pointer;list-style:none;}
|
|
254
334
|
details.help summary::-webkit-details-marker{display:none;}
|
|
255
335
|
details.help summary::after{content:"+";float:right;color:var(--muted);}
|
|
256
336
|
details.help[open] summary::after{content:"-";}
|
|
257
337
|
details.help .body{padding:0 0 16px;font-size:1rem;line-height:1.6;color:var(--ink);}
|
|
258
|
-
details.help .body b{color:var(--
|
|
338
|
+
details.help .body b{color:var(--accent-text);}
|
|
259
339
|
|
|
260
340
|
/* ---- Class picker ---- */
|
|
261
341
|
.classcard{
|
|
262
342
|
display:flex;align-items:center;gap:14px;width:100%;text-align:left;
|
|
263
|
-
background:var(--
|
|
264
|
-
border-radius:var(--radius);padding:16px;margin:12px 0;color:var(--ink);
|
|
343
|
+
background:var(--surface);border:1px solid var(--line);border-left:4px solid var(--accent);
|
|
344
|
+
border-radius:var(--radius);padding:16px;margin:12px 0;color:var(--ink);box-shadow:var(--shadow);
|
|
265
345
|
}
|
|
266
346
|
.classcard:active{transform:translateY(1px);}
|
|
267
|
-
.classcard .ic{font-size:
|
|
347
|
+
.classcard .ic{font-size:2rem;line-height:1;display:inline-flex;color:var(--accent-text);}
|
|
268
348
|
.classcard .txt{flex:1;}
|
|
269
|
-
.classcard .nm{font-size:1.
|
|
270
|
-
.classcard .tl{font-size:.
|
|
271
|
-
.classcard .arr{color:var(--
|
|
349
|
+
.classcard .nm{font-size:1.25rem;font-weight:800;color:var(--ink);}
|
|
350
|
+
.classcard .tl{font-size:.88rem;color:var(--muted);margin-top:2px;}
|
|
351
|
+
.classcard .arr{color:var(--muted);font-size:1.3rem;font-weight:800;}
|
|
272
352
|
.previewtag{
|
|
273
353
|
font-size:.62rem;text-transform:uppercase;letter-spacing:.5px;vertical-align:middle;
|
|
274
|
-
background
|
|
354
|
+
background:color-mix(in srgb, var(--harm) 15%, var(--surface));
|
|
355
|
+
border:1px solid color-mix(in srgb, var(--harm) 40%, var(--line));
|
|
356
|
+
color:var(--harm);border-radius:6px;padding:1px 6px;font-weight:700;
|
|
275
357
|
}
|
|
276
358
|
|
|
277
359
|
/* ---- Browse compact rows (tap to expand) ---- */
|
|
278
360
|
.browse-row{
|
|
279
|
-
background:var(--
|
|
361
|
+
background:var(--surface);border:1px solid var(--line);border-left:4px solid var(--line);
|
|
280
362
|
border-radius:12px;margin:8px 0;overflow:hidden;
|
|
281
363
|
}
|
|
282
|
-
.browse-row.rank0{border-left-color
|
|
283
|
-
.browse-row.rank1{border-left-color
|
|
284
|
-
.browse-row.rank2{border-left-color
|
|
285
|
-
.browse-row.rank3{border-left-color
|
|
286
|
-
.browse-row.rank4{border-left-color
|
|
287
|
-
.browse-row.rank5{border-left-color
|
|
288
|
-
.browse-row.rank6{border-left-color
|
|
289
|
-
.browse-row.rank7{border-left-color
|
|
290
|
-
.browse-row.rank8{border-left-color
|
|
291
|
-
.browse-row.rank9{border-left-color
|
|
292
|
-
.browse-row.rank10{border-left-color:var(--
|
|
364
|
+
.browse-row.rank0{border-left-color:var(--rank0);}
|
|
365
|
+
.browse-row.rank1{border-left-color:var(--rank1);}
|
|
366
|
+
.browse-row.rank2{border-left-color:var(--rank2);}
|
|
367
|
+
.browse-row.rank3{border-left-color:var(--rank3);}
|
|
368
|
+
.browse-row.rank4{border-left-color:var(--rank4);}
|
|
369
|
+
.browse-row.rank5{border-left-color:var(--rank5);}
|
|
370
|
+
.browse-row.rank6{border-left-color:var(--rank6);}
|
|
371
|
+
.browse-row.rank7{border-left-color:var(--rank7);}
|
|
372
|
+
.browse-row.rank8{border-left-color:var(--rank8);}
|
|
373
|
+
.browse-row.rank9{border-left-color:var(--rank9);}
|
|
374
|
+
.browse-row.rank10{border-left-color:var(--rank10);}
|
|
293
375
|
.browse-row-head{
|
|
294
376
|
display:flex;flex-direction:column;align-items:flex-start;gap:2px;
|
|
295
377
|
width:100%;text-align:left;background:none;border:none;color:var(--ink);
|
|
@@ -297,75 +379,83 @@ details.help .body b{color:var(--gold);}
|
|
|
297
379
|
}
|
|
298
380
|
.browse-row-head::after{content:"▾";position:absolute;right:14px;top:14px;color:var(--muted);}
|
|
299
381
|
.browse-row-head.open::after{content:"▴";}
|
|
300
|
-
.browse-row-head .brn{font-size:1.
|
|
382
|
+
.browse-row-head .brn{font-size:1.05rem;font-weight:700;}
|
|
301
383
|
.browse-row-head .brmeta{font-size:.82rem;color:var(--muted);}
|
|
302
|
-
.brfocus{font-size:.62rem;text-transform:uppercase;letter-spacing:.5px;color
|
|
384
|
+
.brfocus{font-size:.62rem;text-transform:uppercase;letter-spacing:.5px;color:var(--accent-text);border:1px solid var(--accent-dim);border-radius:5px;padding:0 5px;vertical-align:middle;}
|
|
303
385
|
.formerly{font-size:.72rem;font-style:italic;font-weight:600;color:var(--muted);white-space:nowrap;}
|
|
304
|
-
.ritualtag{font-size:.7rem;text-transform:uppercase;letter-spacing:.5px;font-weight:800;background
|
|
386
|
+
.ritualtag{font-size:.7rem;text-transform:uppercase;letter-spacing:.5px;font-weight:800;background:var(--accent-soft);border:1px solid var(--accent-dim);color:var(--accent-text);border-radius:6px;padding:1px 7px;}
|
|
305
387
|
.browse-card{padding:0 12px 4px;}
|
|
306
388
|
.browse-card .card{margin:0 0 10px;}
|
|
307
389
|
|
|
308
390
|
/* ---- Header menu button + character menu ---- */
|
|
309
391
|
.tophead{display:flex;align-items:center;justify-content:space-between;gap:10px;}
|
|
310
392
|
.menubtn{
|
|
311
|
-
flex:0 0 auto;background:var(--
|
|
312
|
-
border-radius:12px;width:44px;height:44px;font-size:1.3rem;line-height:1;color:var(--
|
|
393
|
+
flex:0 0 auto;background:var(--surface-2);border:1px solid var(--line);
|
|
394
|
+
border-radius:12px;width:44px;height:44px;font-size:1.3rem;line-height:1;color:var(--accent-text);
|
|
395
|
+
display:inline-flex;align-items:center;justify-content:center;
|
|
313
396
|
}
|
|
314
|
-
.classcard.activechar{border-left-color:var(--
|
|
397
|
+
.classcard.activechar{border-left-color:var(--accent);background:var(--accent-soft);}
|
|
315
398
|
textarea{
|
|
316
|
-
width:100%;padding:
|
|
317
|
-
background:var(--
|
|
399
|
+
width:100%;padding:13px;font-size:1rem;border-radius:12px;line-height:1.4;
|
|
400
|
+
background:var(--surface-2);color:var(--ink);border:1px solid var(--line);
|
|
318
401
|
font-family:inherit;resize:vertical;margin:6px 0;
|
|
319
402
|
}
|
|
320
403
|
|
|
404
|
+
/* ---- Appearance / custom palette ---- */
|
|
405
|
+
.swatchrow{display:flex;align-items:center;gap:10px;margin:8px 0;}
|
|
406
|
+
.swatchrow label{flex:1;font-weight:600;font-size:.95rem;}
|
|
407
|
+
.swatchrow input[type="color"]{flex:0 0 56px;width:56px;}
|
|
408
|
+
|
|
321
409
|
/* ---- Repertoire builder (spontaneous) ---- */
|
|
322
410
|
.rep-row .slotrow{gap:8px;}
|
|
323
411
|
.sigstar{
|
|
324
412
|
width:38px;height:38px;flex:0 0 38px;border-radius:10px;
|
|
325
|
-
background:var(--
|
|
413
|
+
background:var(--surface-2);border:1px solid var(--accent-dim);color:var(--muted);
|
|
326
414
|
font-size:1.2rem;line-height:1;display:flex;align-items:center;justify-content:center;
|
|
327
415
|
}
|
|
328
|
-
.sigstar.on{background:var(--
|
|
416
|
+
.sigstar.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
|
|
329
417
|
.rmrow{
|
|
330
418
|
width:38px;height:38px;flex:0 0 38px;border-radius:10px;
|
|
331
|
-
background:var(--
|
|
419
|
+
background:var(--surface-2);border:1px solid var(--line);color:var(--muted);font-weight:700;
|
|
332
420
|
}
|
|
333
|
-
.focus-checklist{max-height:320px;overflow-y:auto;border:1px solid var(--line);border-radius:12px;margin-top:8px;background:var(--
|
|
421
|
+
.focus-checklist{max-height:320px;overflow-y:auto;border:1px solid var(--line);border-radius:12px;margin-top:8px;background:var(--surface-2);}
|
|
334
422
|
.fcheck{display:flex;align-items:center;gap:10px;padding:11px 12px;border-bottom:1px solid var(--line);cursor:pointer;}
|
|
335
423
|
.fcheck:last-child{border-bottom:none;}
|
|
336
424
|
.fcheck input{width:22px;height:22px;flex:0 0 22px;}
|
|
337
425
|
.fcheck .fcname{flex:1;font-weight:600;}
|
|
338
|
-
.fcheck .fcrank{font-size:.75rem;color:var(--muted);background:var(--
|
|
426
|
+
.fcheck .fcrank{font-size:.75rem;color:var(--muted);background:var(--surface);border:1px solid var(--line);border-radius:6px;padding:1px 7px;}
|
|
339
427
|
|
|
340
428
|
.btn.addrow{
|
|
341
429
|
display:inline-block;width:auto;padding:10px 16px;margin:4px 0 0 44px;
|
|
342
|
-
font-size:.95rem;font-weight:700;border-radius:10px;
|
|
343
|
-
background:var(--
|
|
430
|
+
font-size:.95rem;font-weight:700;border-radius:10px;box-shadow:none;
|
|
431
|
+
background:var(--surface-2);color:var(--accent-text);border:1px solid var(--accent-dim);
|
|
344
432
|
}
|
|
345
433
|
.slottag{
|
|
346
|
-
|
|
347
|
-
|
|
434
|
+
display:inline-flex;align-items:center;gap:4px;
|
|
435
|
+
font-size:.85rem;background:var(--surface-2);border:1px solid var(--accent-dim);
|
|
436
|
+
color:var(--accent-text);border-radius:6px;padding:1px 6px;margin-right:4px;
|
|
348
437
|
}
|
|
438
|
+
.slottag .icn{width:.95rem;height:.95rem;}
|
|
349
439
|
|
|
350
440
|
/* ---- Bottom nav ---- */
|
|
351
441
|
nav.bottom{
|
|
352
442
|
position:fixed;bottom:0;left:0;right:0;z-index:30;
|
|
353
|
-
background
|
|
443
|
+
background:var(--surface);border-top:1px solid var(--line);
|
|
354
444
|
display:flex;justify-content:space-around;
|
|
355
445
|
padding:6px 4px calc(6px + env(safe-area-inset-bottom));
|
|
356
446
|
}
|
|
357
447
|
nav.bottom button{
|
|
358
448
|
flex:1;background:none;border:none;color:var(--muted);
|
|
359
|
-
display:flex;flex-direction:column;align-items:center;gap:
|
|
360
|
-
padding:8px 2px;font-size:.
|
|
449
|
+
display:flex;flex-direction:column;align-items:center;gap:3px;
|
|
450
|
+
padding:8px 2px;font-size:.7rem;font-weight:700;
|
|
361
451
|
}
|
|
362
|
-
nav.bottom button .ic{font-size:1.
|
|
363
|
-
nav.bottom button.on{color:var(--
|
|
452
|
+
nav.bottom button .ic{font-size:1.4rem;line-height:1;display:inline-flex;}
|
|
453
|
+
nav.bottom button.on{color:var(--accent-text);}
|
|
364
454
|
.toast{
|
|
365
455
|
position:fixed;bottom:96px;left:50%;transform:translateX(-50%);
|
|
366
|
-
background:var(--
|
|
456
|
+
background:var(--accent);color:var(--accent-ink);font-weight:800;padding:12px 20px;
|
|
367
457
|
border-radius:30px;z-index:50;opacity:0;transition:opacity .25s;pointer-events:none;
|
|
368
|
-
box-shadow:0 4px 16px rgba(0,0,0,.
|
|
458
|
+
box-shadow:0 4px 16px rgba(0,0,0,.35);
|
|
369
459
|
}
|
|
370
460
|
.toast.show{opacity:1;}
|
|
371
461
|
|
|
@@ -375,8 +465,8 @@ nav.bottom button.on{color:var(--gold);}
|
|
|
375
465
|
|
|
376
466
|
<header class="top hide">
|
|
377
467
|
<div class="tophead">
|
|
378
|
-
<h1><span id="charIcon"
|
|
379
|
-
<button class="menubtn" onclick="openMenu()" title="Characters & backup" aria-label="Characters menu"
|
|
468
|
+
<h1><span id="charIcon"></span> <span id="charNameHead">Spellbook</span></h1>
|
|
469
|
+
<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>
|
|
380
470
|
</div>
|
|
381
471
|
<div class="sub" id="charSub">Pathfinder 2e (Remaster)</div>
|
|
382
472
|
<div class="statbar" id="statbar">
|
|
@@ -391,11 +481,14 @@ nav.bottom button.on{color:var(--gold);}
|
|
|
391
481
|
|
|
392
482
|
<!-- ================= CHARACTER MENU ================= -->
|
|
393
483
|
<section id="view-menu" class="wrap hide">
|
|
394
|
-
<h2
|
|
484
|
+
<h2>Appearance</h2>
|
|
485
|
+
<div id="appearancePanel"></div>
|
|
486
|
+
|
|
487
|
+
<h2 style="margin-top:24px">Use offline</h2>
|
|
395
488
|
<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>
|
|
396
489
|
<div class="row">
|
|
397
|
-
<button class="btn" onclick="downloadOffline()"
|
|
398
|
-
<button class="btn secondary" id="installBtn" style="display:none" onclick="installApp()"
|
|
490
|
+
<button class="btn" onclick="downloadOffline()">Save offline copy</button>
|
|
491
|
+
<button class="btn secondary" id="installBtn" style="display:none" onclick="installApp()">Install app</button>
|
|
399
492
|
</div>
|
|
400
493
|
<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>
|
|
401
494
|
|
|
@@ -407,10 +500,15 @@ nav.bottom button.on{color:var(--gold);}
|
|
|
407
500
|
<h2 style="margin-top:24px">Share & back up</h2>
|
|
408
501
|
<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>
|
|
409
502
|
<div class="row">
|
|
410
|
-
<button class="btn secondary" onclick="exportCharacter()"
|
|
411
|
-
<button class="btn secondary" onclick="importCharacter()"
|
|
503
|
+
<button class="btn secondary" onclick="exportCharacter()">Export active</button>
|
|
504
|
+
<button class="btn secondary" onclick="importCharacter()">Import code</button>
|
|
412
505
|
</div>
|
|
413
506
|
<textarea id="menuIO" rows="3" placeholder="Character code appears here on Export — or paste one here, then tap Import."></textarea>
|
|
507
|
+
|
|
508
|
+
<h2 style="margin-top:24px">Support</h2>
|
|
509
|
+
<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>
|
|
510
|
+
<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>
|
|
511
|
+
|
|
414
512
|
<button class="btn secondary" onclick="closeMenu()">← Back</button>
|
|
415
513
|
<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>
|
|
416
514
|
</section>
|
|
@@ -428,7 +526,7 @@ nav.bottom button.on{color:var(--gold);}
|
|
|
428
526
|
<!-- ================= PREPARE / REPERTOIRE ================= -->
|
|
429
527
|
<section id="view-prepare" class="wrap hide">
|
|
430
528
|
<p class="meta" id="prepClassLine"></p>
|
|
431
|
-
<div class="banner hide" id="prepPreviewBanner"
|
|
529
|
+
<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>
|
|
432
530
|
|
|
433
531
|
<h2>① Your character</h2>
|
|
434
532
|
<p class="meta">Copy these from your character sheet.</p>
|
|
@@ -465,9 +563,9 @@ nav.bottom button.on{color:var(--gold);}
|
|
|
465
563
|
<!-- ================= BROWSE ================= -->
|
|
466
564
|
<section id="view-browse" class="wrap hide">
|
|
467
565
|
<h2 id="browseTitle">All spells</h2>
|
|
468
|
-
<input type="text" id="search" placeholder="
|
|
566
|
+
<input type="text" id="search" placeholder="Search spells…" oninput="renderBrowse()">
|
|
469
567
|
<div class="filterbar" id="rankChips"></div>
|
|
470
|
-
<button class="chip" id="filterToggle" onclick="toggleBrowseFilters()" aria-expanded="false"
|
|
568
|
+
<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>
|
|
471
569
|
<div id="browseFilters" class="hide"></div>
|
|
472
570
|
<div id="browseList"></div>
|
|
473
571
|
</section>
|
|
@@ -482,10 +580,10 @@ nav.bottom button.on{color:var(--gold);}
|
|
|
482
580
|
<div class="toast" id="toast"></div>
|
|
483
581
|
|
|
484
582
|
<nav class="bottom hide">
|
|
485
|
-
<button id="nav-today" class="on" onclick="go('today')"><span class="ic"
|
|
486
|
-
<button id="nav-prepare" onclick="go('prepare')"><span class="ic"
|
|
487
|
-
<button id="nav-browse" onclick="go('browse')"><span class="ic"
|
|
488
|
-
<button id="nav-help" onclick="go('help')"><span class="ic"
|
|
583
|
+
<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>
|
|
584
|
+
<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>
|
|
585
|
+
<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>
|
|
586
|
+
<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>
|
|
489
587
|
</nav>
|
|
490
588
|
|
|
491
589
|
<script>
|
|
@@ -58710,6 +58808,46 @@ function psychicSlots(level){
|
|
|
58710
58808
|
return slots;
|
|
58711
58809
|
}
|
|
58712
58810
|
|
|
58811
|
+
/* ============================================================
|
|
58812
|
+
INLINE SVG ICON REGISTRY
|
|
58813
|
+
Thin line icons (24x24, stroke = currentColor) so they inherit the
|
|
58814
|
+
active accent colour. Keyed by name; classes/features reference a key
|
|
58815
|
+
instead of an emoji. iconSvg() wraps the path data in an <svg>.
|
|
58816
|
+
============================================================ */
|
|
58817
|
+
const ICONS={
|
|
58818
|
+
/* classes */
|
|
58819
|
+
cleric:'<circle cx="12" cy="12" r="3.4"/><path d="M12 3v2.6M12 18.4V21M3 12h2.6M18.4 12H21M5.6 5.6l1.9 1.9M16.5 16.5l1.9 1.9M18.4 5.6l-1.9 1.9M7.5 16.5l-1.9 1.9"/>',
|
|
58820
|
+
wizard:'<path d="M12 3.5 4.5 19.5h15z"/><circle cx="12" cy="13.5" r="1.9"/>',
|
|
58821
|
+
druid:'<path d="M5 19.5C5 11 10.5 5 19 4.5 19 13 13.5 19 5 19.5z"/><path d="M11.5 12 5 19.5"/>',
|
|
58822
|
+
witch:'<path d="M17.5 15.2A6 6 0 1 1 11.3 5a5 5 0 0 0 6.2 10.2z"/><path d="M18.3 3.6l.8 1.9 1.9.8-1.9.8-.8 1.9-.8-1.9-1.9-.8 1.9-.8z"/>',
|
|
58823
|
+
sorcerer:'<path d="M12 3c2.8 3.6 4.6 5.6 4.6 8.4a4.6 4.6 0 0 1-9.2 0c0-1.2.6-2.2 1.6-3.1.3 1.1 1 1.6 1.6 1.6C9.4 8.4 10.3 5.7 12 3z"/>',
|
|
58824
|
+
bard:'<path d="M9.5 17.5V6.2l9-2v9.6"/><circle cx="6.8" cy="17.5" r="2.4"/><circle cx="15.8" cy="15.8" r="2.4"/>',
|
|
58825
|
+
oracle:'<path d="M2.5 12S6 6 12 6s9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6z"/><circle cx="12" cy="12" r="2.4"/>',
|
|
58826
|
+
magus:'<path d="M17.5 3.5 20.5 6.5 12 15l-3 1 1-3z"/><path d="M8 14l-4 4"/><path d="M5.5 15.5l3 3"/>',
|
|
58827
|
+
summoner:'<circle cx="9.2" cy="12" r="4.8"/><circle cx="14.8" cy="12" r="4.8"/>',
|
|
58828
|
+
psychic:'<path d="M12 12a1.5 1.5 0 1 0 1.6-1.5A3.6 3.6 0 1 0 8.4 12 5.7 5.7 0 1 0 17.7 7.7"/>',
|
|
58829
|
+
animist:'<path d="M12 3c2 2.5 3.1 4 3.1 6a3.1 3.1 0 0 1-6.2 0c0-1 .5-2 1.5-2.9.3.8.8 1.2 1.3 1.2C11 6 11 5 12 3z"/><path d="M12 12v6"/><path d="M9 21h6"/>',
|
|
58830
|
+
/* navigation */
|
|
58831
|
+
today:'<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"/>',
|
|
58832
|
+
prepare:'<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"/>',
|
|
58833
|
+
browse:'<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"/>',
|
|
58834
|
+
help:'<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"/>',
|
|
58835
|
+
/* features / utility */
|
|
58836
|
+
school:'<path d="M2.5 8.5 12 4.5l9.5 4-9.5 4z"/><path d="M6.5 10.5v4c0 1.5 11 1.5 11 0v-4"/><path d="M21.5 8.5v5"/>',
|
|
58837
|
+
bonded:'<path d="M9.3 12.4a3 3 0 0 1 0-4.2l1.9-1.9a3 3 0 0 1 4.2 4.2l-1 1"/><path d="M14.7 11.6a3 3 0 0 1 0 4.2l-1.9 1.9a3 3 0 0 1-4.2-4.2l1-1"/>',
|
|
58838
|
+
heal:'<path d="M12 5.5v13M5.5 12h13"/>',
|
|
58839
|
+
harm:'<path d="M12 3a7 7 0 0 0-7 7c0 2 1 3.6 2.2 4.6V17a1 1 0 0 0 1 1h7.6a1 1 0 0 0 1-1v-2.4C19 13.6 20 12 20 10a7 7 0 0 0-7-7z" transform="translate(-1 0)"/><circle cx="8.5" cy="10.5" r="1.1"/><circle cx="13.5" cy="10.5" r="1.1"/><path d="M9 18v2M11 18v2M13 18v2"/>',
|
|
58840
|
+
menu:'<circle cx="12" cy="8" r="3.4"/><path d="M5.5 20a6.5 6.5 0 0 1 13 0"/>',
|
|
58841
|
+
filter:'<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"/>',
|
|
58842
|
+
plus:'<path d="M12 5.5v13M5.5 12h13"/>',
|
|
58843
|
+
book:'<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"/>',
|
|
58844
|
+
};
|
|
58845
|
+
function iconSvg(name, cls){
|
|
58846
|
+
return '<svg class="icn '+(cls||"")+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" '+
|
|
58847
|
+
'stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'+
|
|
58848
|
+
(ICONS[name]||"")+'</svg>';
|
|
58849
|
+
}
|
|
58850
|
+
|
|
58713
58851
|
/* [minLevel, proficiency bonus] for spell DC / spell attack, highest first.
|
|
58714
58852
|
Bonus = 2/4/6/8 for trained/expert/master/legendary.
|
|
58715
58853
|
Wizard, Druid, Sorcerer, Bard, Oracle, Psychic and the Cloistered Cleric
|
|
@@ -58757,13 +58895,13 @@ const CORE_HELP=[
|
|
|
58757
58895
|
["Remaster vs. legacy (original) rules",
|
|
58758
58896
|
"This app uses the current <b>Remaster</b> spell names and rules. If your group still plays the <b>original</b> (pre-Remaster) edition, most spells are identical — and renamed ones show a small <i>“formerly …”</i> note, so you can also <b>search by the old name</b> (e.g. searching <b>Magic Missile</b> finds <b>Force Barrage</b>). A few legacy spells that weren't changed are still listed under their original names too.<br><br>A few wording changes to keep in mind: spell <b>rank</b> = spell <b>level</b>; <b>vitality</b>/<b>void</b> damage = the old <b>positive</b>/<b>negative</b>; and <b>holy</b>/<b>unholy</b> replace the old <b>good</b>/<b>evil</b> alignment damage. The numbers and effects are the same."],
|
|
58759
58897
|
["Multiple characters, backup & sharing",
|
|
58760
|
-
"Tap the <b
|
|
58898
|
+
"Tap the <b>character</b> button (top right) to keep <b>several characters</b> and switch between them — each remembers its own prepared spells. From there you can also <b>Export</b> a character to a code to back it up or send to a friend, and <b>Import</b> a code someone shares with you. Everything is stored on your device."],
|
|
58761
58899
|
];
|
|
58762
58900
|
|
|
58763
58901
|
const CLASSES={
|
|
58764
58902
|
/* ---------------- CLERIC ---------------- */
|
|
58765
58903
|
cleric:{
|
|
58766
|
-
id:"cleric", name:"Cleric", icon:"
|
|
58904
|
+
id:"cleric", name:"Cleric", icon:"cleric", color:"#d8a23a",
|
|
58767
58905
|
tagline:"Divine prepared caster · the party's main healer",
|
|
58768
58906
|
tradition:"divine", keyAbility:"Wisdom", keyAbilityShort:"Wis",
|
|
58769
58907
|
casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
|
|
@@ -58786,63 +58924,59 @@ const CLASSES={
|
|
|
58786
58924
|
|
|
58787
58925
|
/* ---------------- WIZARD ---------------- */
|
|
58788
58926
|
wizard:{
|
|
58789
|
-
id:"wizard", name:"Wizard", icon:"
|
|
58927
|
+
id:"wizard", name:"Wizard", icon:"wizard", color:"#7b6cf0",
|
|
58790
58928
|
tagline:"Arcane prepared caster · the widest spell list",
|
|
58791
58929
|
tradition:"arcane", keyAbility:"Intelligence", keyAbilityShort:"Int",
|
|
58792
58930
|
casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
|
|
58793
|
-
preview:true,
|
|
58794
58931
|
features:[
|
|
58795
|
-
{type:"extraSlots", key:"school", icon:"
|
|
58932
|
+
{type:"extraSlots", key:"school", icon:"school", label:"Arcane school slot",
|
|
58796
58933
|
note:"One extra slot of each rank you can cast, which must hold a spell from your chosen school / curriculum."},
|
|
58797
|
-
{type:"dailyResource", key:"bonded", icon:"
|
|
58934
|
+
{type:"dailyResource", key:"bonded", icon:"bonded", label:"Drain Bonded Item", uses:1,
|
|
58798
58935
|
note:"Once per day, recast a spell you already cast today without spending its slot."},
|
|
58799
58936
|
],
|
|
58800
58937
|
help:[
|
|
58801
58938
|
["What does a wizard do?",
|
|
58802
58939
|
"You're an <b>Arcane</b> prepared caster with the broadest spell list in the game. You Prepare spells each morning from your spellbook, leaning on control and damage like <b>Fireball</b> plus utility."],
|
|
58803
58940
|
["Arcane school slot",
|
|
58804
|
-
"Your school (curriculum) gives you <b>one extra prepared slot of every rank</b> you can cast. Fill the
|
|
58941
|
+
"Your school (curriculum) gives you <b>one extra prepared slot of every rank</b> you can cast. Fill the <b>school</b> slots with spells from your school. The app adds these slots automatically."],
|
|
58805
58942
|
["Drain Bonded Item",
|
|
58806
|
-
"Once per day you can <b>Drain your Bonded Item</b> to recast a spell you've already cast that day, without spending another slot. Tick the
|
|
58943
|
+
"Once per day you can <b>Drain your Bonded Item</b> to recast a spell you've already cast that day, without spending another slot. Tick the <b>Drain Bonded Item</b> tracker on Cast Today when you use it."],
|
|
58807
58944
|
],
|
|
58808
58945
|
},
|
|
58809
58946
|
|
|
58810
58947
|
/* ---------------- DRUID ---------------- */
|
|
58811
58948
|
druid:{
|
|
58812
|
-
id:"druid", name:"Druid", icon:"
|
|
58949
|
+
id:"druid", name:"Druid", icon:"druid", color:"#4faf6d",
|
|
58813
58950
|
tagline:"Primal prepared caster · nature's versatility",
|
|
58814
58951
|
tradition:"primal", keyAbility:"Wisdom", keyAbilityShort:"Wis",
|
|
58815
58952
|
casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
|
|
58816
|
-
preview:true,
|
|
58817
58953
|
features:[{type:"note", title:"Druidic order", text:"Pick your order's focus spells (Heal Animal, Tempest Surge, Wild/Untamed Form…) in the Focus section below. Companion/familiar bookkeeping stays on your sheet."}],
|
|
58818
58954
|
help:[
|
|
58819
58955
|
["What does a druid do?",
|
|
58820
|
-
"You're a <b>Primal</b> prepared caster tied to nature, with strong healing, control, and battlefield spells. Many druids also use order focus spells to shapeshift
|
|
58956
|
+
"You're a <b>Primal</b> prepared caster tied to nature, with strong healing, control, and battlefield spells. Many druids also use order focus spells to shapeshift — add the ones you know in the Focus section."],
|
|
58821
58957
|
],
|
|
58822
58958
|
},
|
|
58823
58959
|
|
|
58824
58960
|
/* ---------------- WITCH ---------------- */
|
|
58825
58961
|
witch:{
|
|
58826
|
-
id:"witch", name:"Witch", icon:"
|
|
58962
|
+
id:"witch", name:"Witch", icon:"witch", color:"#b061b8",
|
|
58827
58963
|
tagline:"Prepared caster · your patron sets the tradition",
|
|
58828
58964
|
traditionFrom:"patron", defaultTradition:"occult",
|
|
58829
58965
|
keyAbility:"Intelligence", keyAbilityShort:"Int",
|
|
58830
58966
|
casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
|
|
58831
|
-
preview:true,
|
|
58832
58967
|
features:[{type:"note", title:"Hexes & familiar", text:"Choose your patron's tradition above, then add your hex focus spells in the Focus section below. Familiar details stay on your sheet."}],
|
|
58833
58968
|
help:[
|
|
58834
58969
|
["What does a witch do?",
|
|
58835
|
-
"You're a prepared caster whose <b>patron</b> grants your magic — set your patron's <b>tradition</b> (arcane, divine, occult, or primal) in the character section, and your spell list follows. Witches also cast <b>hexes</b>
|
|
58970
|
+
"You're a prepared caster whose <b>patron</b> grants your magic — set your patron's <b>tradition</b> (arcane, divine, occult, or primal) in the character section, and your spell list follows. Witches also cast <b>hexes</b> — add your hex focus spells in the Focus section (your familiar stays on your sheet)."],
|
|
58836
58971
|
],
|
|
58837
58972
|
},
|
|
58838
58973
|
|
|
58839
58974
|
/* ---------------- SORCERER ---------------- */
|
|
58840
58975
|
sorcerer:{
|
|
58841
|
-
id:"sorcerer", name:"Sorcerer", icon:"
|
|
58976
|
+
id:"sorcerer", name:"Sorcerer", icon:"sorcerer", color:"#e05a52",
|
|
58842
58977
|
tagline:"Spontaneous caster · magic in the blood",
|
|
58843
58978
|
traditionFrom:"bloodline", keyAbility:"Charisma", keyAbilityShort:"Cha",
|
|
58844
58979
|
casting:"spontaneous", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
|
|
58845
|
-
preview:true,
|
|
58846
58980
|
features:[
|
|
58847
58981
|
{type:"signature"},
|
|
58848
58982
|
{type:"note", title:"Bloodline magic", text:"Add your bloodline focus spells in the Focus section below. Blood-magic side effects are described on each spell but tracked by you."},
|
|
@@ -58857,18 +58991,17 @@ const CLASSES={
|
|
|
58857
58991
|
|
|
58858
58992
|
/* ---------------- BARD ---------------- */
|
|
58859
58993
|
bard:{
|
|
58860
|
-
id:"bard", name:"Bard", icon:"
|
|
58994
|
+
id:"bard", name:"Bard", icon:"bard", color:"#34a8c4",
|
|
58861
58995
|
tagline:"Occult spontaneous caster · master of support",
|
|
58862
58996
|
tradition:"occult", keyAbility:"Charisma", keyAbilityShort:"Cha",
|
|
58863
58997
|
casting:"spontaneous", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
|
|
58864
|
-
preview:true,
|
|
58865
58998
|
features:[
|
|
58866
58999
|
{type:"signature"},
|
|
58867
59000
|
{type:"note", title:"Compositions & muse", text:"Add your composition focus spells (Courageous Anthem, Counter Performance…) in the Focus section below."},
|
|
58868
59001
|
],
|
|
58869
59002
|
help:[
|
|
58870
59003
|
["What does a bard do?",
|
|
58871
|
-
"You're an <b>Occult</b> <b>spontaneous</b> caster and the game's best supporter. You build a repertoire and cast flexibly, and your composition cantrips/focus spells buff the party
|
|
59004
|
+
"You're an <b>Occult</b> <b>spontaneous</b> caster and the game's best supporter. You build a repertoire and cast flexibly, and your composition cantrips/focus spells buff the party — add them in the Focus section."],
|
|
58872
59005
|
["Signature spells",
|
|
58873
59006
|
"Mark spells as <b>signature</b> (the ★) to heighten them with higher-rank slots. One per rank is the usual choice."],
|
|
58874
59007
|
],
|
|
@@ -58876,30 +59009,35 @@ const CLASSES={
|
|
|
58876
59009
|
|
|
58877
59010
|
/* ---------------- ORACLE ---------------- */
|
|
58878
59011
|
oracle:{
|
|
58879
|
-
id:"oracle", name:"Oracle", icon:"
|
|
59012
|
+
id:"oracle", name:"Oracle", icon:"oracle", color:"#c061d6",
|
|
58880
59013
|
tagline:"Divine spontaneous caster · power at a price",
|
|
58881
59014
|
tradition:"divine", keyAbility:"Charisma", keyAbilityShort:"Cha",
|
|
58882
59015
|
casting:"spontaneous", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
|
|
58883
|
-
preview:true,
|
|
58884
59016
|
features:[
|
|
58885
59017
|
{type:"signature"},
|
|
58886
|
-
{type:"
|
|
59018
|
+
{type:"stageTracker", key:"curse", icon:"oracle", label:"Oracular curse", max:4,
|
|
59019
|
+
note:"Your curse deepens by a stage each time you use a cursebound action — stronger effects, worse drawbacks. Resting clears it."},
|
|
59020
|
+
{type:"note", title:"Mystery & revelations", text:"Add your revelation focus spells in the Focus section below, and track your escalating oracular curse on Cast Today."},
|
|
58887
59021
|
],
|
|
58888
59022
|
help:[
|
|
58889
59023
|
["What does an oracle do?",
|
|
58890
|
-
"You're a <b>Divine</b> <b>spontaneous</b> caster channeling a <b>mystery</b>. You build a repertoire and cast flexibly
|
|
59024
|
+
"You're a <b>Divine</b> <b>spontaneous</b> caster channeling a <b>mystery</b>. You build a repertoire and cast flexibly. Add your revelation focus spells in the Focus section; your escalating <b>curse</b> is tracked on your sheet."],
|
|
58891
59025
|
["Signature spells",
|
|
58892
59026
|
"Mark spells as <b>signature</b> (the ★) to heighten them with higher-rank slots — usually one per spell rank."],
|
|
58893
59027
|
],
|
|
58894
59028
|
},
|
|
58895
59029
|
/* ---------------- MAGUS ---------------- */
|
|
58896
59030
|
magus:{
|
|
58897
|
-
id:"magus", name:"Magus", icon:"
|
|
59031
|
+
id:"magus", name:"Magus", icon:"magus", color:"#5d82e0",
|
|
58898
59032
|
tagline:"Arcane half-caster · blade and spell as one",
|
|
58899
59033
|
tradition:"arcane", keyAbility:"Intelligence", keyAbilityShort:"Int",
|
|
58900
59034
|
casting:"prepared", slots:"partial", prof:PARTIAL_CASTER_PROF, cantrips:5,
|
|
58901
|
-
|
|
58902
|
-
|
|
59035
|
+
features:[
|
|
59036
|
+
{type:"studiousSlots", key:"studious", minLevel:7, rank:2, count:2, label:"Studious spells",
|
|
59037
|
+
spells:["gecko-grip","sure-strike","water-breathing"],
|
|
59038
|
+
note:"From level 7, two extra 2nd-rank slots for utility spells only: Gecko Grip, Sure Strike, or Water Breathing — plus one spell from your hybrid study, which you add on your sheet."},
|
|
59039
|
+
{type:"note", title:"Spellstrike & conflux spells", text:"Add your conflux focus spells (Force Fang, etc.) in the Focus section. Spellstrike itself is a martial action recharged by Arcane Cascade — track it on your sheet. Your main spell slots are always your top two ranks."},
|
|
59040
|
+
],
|
|
58903
59041
|
help:[
|
|
58904
59042
|
["What does a magus do?",
|
|
58905
59043
|
"You're a martial <b>Arcane</b> caster who fuses weapon strikes with spells via <b>Spellstrike</b>. You only get a <b>handful of spell slots</b> (always your two highest ranks), so each prepared spell is precious — often a damaging spell to channel through Spellstrike."],
|
|
@@ -58910,14 +59048,13 @@ const CLASSES={
|
|
|
58910
59048
|
|
|
58911
59049
|
/* ---------------- SUMMONER ---------------- */
|
|
58912
59050
|
summoner:{
|
|
58913
|
-
id:"summoner", name:"Summoner", icon:"
|
|
59051
|
+
id:"summoner", name:"Summoner", icon:"summoner", color:"#2bb59a",
|
|
58914
59052
|
tagline:"Half-caster · you and your eidolon, as one",
|
|
58915
59053
|
traditionFrom:"patron", defaultTradition:"arcane",
|
|
58916
59054
|
traditionChoiceLabel:"Eidolon's tradition",
|
|
58917
59055
|
traditionChoiceHint:"your eidolon's essence sets your spell tradition",
|
|
58918
59056
|
keyAbility:"Charisma", keyAbilityShort:"Cha",
|
|
58919
59057
|
casting:"spontaneous", slots:"partial", prof:PARTIAL_CASTER_PROF, cantrips:5,
|
|
58920
|
-
preview:true,
|
|
58921
59058
|
features:[
|
|
58922
59059
|
{type:"signature"},
|
|
58923
59060
|
{type:"note", title:"Eidolon & shared life", text:"Choose your eidolon's tradition above and add evolution focus spells in the Focus section. You get only a few slots (top two ranks). Your shared HP pool and the eidolon's actions stay on your sheet."},
|
|
@@ -58932,14 +59069,15 @@ const CLASSES={
|
|
|
58932
59069
|
|
|
58933
59070
|
/* ---------------- PSYCHIC ---------------- */
|
|
58934
59071
|
psychic:{
|
|
58935
|
-
id:"psychic", name:"Psychic", icon:"
|
|
59072
|
+
id:"psychic", name:"Psychic", icon:"psychic", color:"#e05fa6",
|
|
58936
59073
|
tagline:"Occult spontaneous caster · few spells, mighty cantrips",
|
|
58937
59074
|
tradition:"occult", keyAbility:"Int or Cha", keyAbilityShort:"Key",
|
|
58938
|
-
casting:"spontaneous", slots:"psychic", prof:FULL_CASTER_PROF, cantrips:
|
|
58939
|
-
preview:true,
|
|
59075
|
+
casting:"spontaneous", slots:"psychic", prof:FULL_CASTER_PROF, cantrips:3,
|
|
58940
59076
|
features:[
|
|
58941
59077
|
{type:"signature"},
|
|
58942
|
-
{type:"
|
|
59078
|
+
{type:"dailyResource", key:"unleash", icon:"psychic", label:"Unleash Psyche", uses:1, verb:"Unleash", recharge:"refocus",
|
|
59079
|
+
note:"Surge your psyche for 2 rounds — bigger cantrips and better spell attacks. Afterward your mind is strained; Refocus to Unleash again."},
|
|
59080
|
+
{type:"note", title:"Psi cantrips & Amps", text:"Your psi cantrips appear in the Focus section — cast them at will, or spend a Focus Point to Amp them (tap Amp). Refocus to recover points. Your spell DC uses Intelligence or Charisma, whichever your conscious mind chose."},
|
|
58943
59081
|
],
|
|
58944
59082
|
help:[
|
|
58945
59083
|
["What does a psychic do?",
|
|
@@ -58951,15 +59089,14 @@ const CLASSES={
|
|
|
58951
59089
|
|
|
58952
59090
|
/* ---------------- ANIMIST ---------------- */
|
|
58953
59091
|
animist:{
|
|
58954
|
-
id:"animist", name:"Animist", icon:"
|
|
59092
|
+
id:"animist", name:"Animist", icon:"animist", color:"#e08a3c",
|
|
58955
59093
|
tagline:"Divine prepared caster · channels apparitions (spirits)",
|
|
58956
59094
|
tradition:"divine", keyAbility:"Wisdom", keyAbilityShort:"Wis",
|
|
58957
59095
|
casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:4,
|
|
58958
|
-
preview:true,
|
|
58959
59096
|
features:[{type:"note", title:"Apparitions & vessel spells", text:"You prepare divine spells; your attuned apparitions grant a vessel focus spell (add the ones you know in the Focus section below) and a separate spontaneous repertoire of apparition spells that stays on your character sheet."}],
|
|
58960
59097
|
help:[
|
|
58961
59098
|
["What does an animist do?",
|
|
58962
|
-
"You're a <b>Divine</b> prepared caster who channels <b>apparitions</b> — spirits that lend you power. You prepare divine spells like a cleric, and your attuned apparitions grant extra spells plus a <b>vessel</b> focus spell. The app tracks your divine slots, cantrips and DC; apparition attunement and the apparition repertoire stay on your sheet
|
|
59099
|
+
"You're a <b>Divine</b> prepared caster who channels <b>apparitions</b> — spirits that lend you power. You prepare divine spells like a cleric, and your attuned apparitions grant extra spells plus a <b>vessel</b> focus spell. The app tracks your divine slots, cantrips and DC; apparition attunement and the apparition repertoire stay on your character sheet."],
|
|
58963
59100
|
["Cantrips & apparitions",
|
|
58964
59101
|
"You know <b>4 cantrips</b> — two you choose plus two granted by your attuned apparitions. Your <b>vessel</b> focus spell (from your primary apparition) appears in the Focus section; cast it with Focus Points like other focus spells."],
|
|
58965
59102
|
],
|
|
@@ -59050,14 +59187,16 @@ function loadLibrary(){
|
|
|
59050
59187
|
}catch(e){}
|
|
59051
59188
|
try{ // migrate a single v2 character
|
|
59052
59189
|
const v2=JSON.parse(localStorage.getItem(OLD_KEY));
|
|
59053
|
-
if(v2 && v2.classId){ const c=Object.assign(blankChar(), v2); return {characters:{[c.id]:c}, activeId:c.id}; }
|
|
59190
|
+
if(v2 && v2.classId){ const c=Object.assign(blankChar(), v2); return normalizeLib({characters:{[c.id]:c}, activeId:c.id}); }
|
|
59054
59191
|
}catch(e){}
|
|
59055
59192
|
const c=blankChar();
|
|
59056
|
-
return {characters:{[c.id]:c}, activeId:c.id};
|
|
59193
|
+
return normalizeLib({characters:{[c.id]:c}, activeId:c.id});
|
|
59057
59194
|
}
|
|
59195
|
+
function defaultSettings(){ return { themeMode:"auto", custom:null }; }
|
|
59058
59196
|
function normalizeLib(lib){
|
|
59059
59197
|
Object.keys(lib.characters).forEach(id=>{ lib.characters[id]=Object.assign(defaultStateFields(), lib.characters[id], {id}); });
|
|
59060
59198
|
if(!lib.characters[lib.activeId]) lib.activeId=Object.keys(lib.characters)[0];
|
|
59199
|
+
lib.settings=Object.assign(defaultSettings(), lib.settings||{});
|
|
59061
59200
|
return lib;
|
|
59062
59201
|
}
|
|
59063
59202
|
let _storageWarned=false;
|
|
@@ -59065,9 +59204,66 @@ function saveState(){
|
|
|
59065
59204
|
library.characters[state.id]=state;
|
|
59066
59205
|
try{ localStorage.setItem(LS_KEY, JSON.stringify(library)); }
|
|
59067
59206
|
catch(e){
|
|
59068
|
-
if(!_storageWarned && typeof toast==="function"){ toast("
|
|
59207
|
+
if(!_storageWarned && typeof toast==="function"){ toast("Couldn't save — storage full or disabled"); _storageWarned=true; }
|
|
59069
59208
|
}
|
|
59070
59209
|
}
|
|
59210
|
+
function saveSettings(){ try{ localStorage.setItem(LS_KEY, JSON.stringify(library)); }catch(e){} }
|
|
59211
|
+
|
|
59212
|
+
/* ============================================================
|
|
59213
|
+
THEME (light/dark · per-class accent · custom palette)
|
|
59214
|
+
The base palette flips with [data-theme]; --accent (and its readable
|
|
59215
|
+
ink) is set per active class, or overridden by a custom palette.
|
|
59216
|
+
============================================================ */
|
|
59217
|
+
const DEFAULT_ACCENT="#6c7a89";
|
|
59218
|
+
function hexToRgb(h){
|
|
59219
|
+
h=(h||"").replace("#","").trim();
|
|
59220
|
+
if(h.length===3) h=h.split("").map(c=>c+c).join("");
|
|
59221
|
+
const n=parseInt(h||"000000",16);
|
|
59222
|
+
return {r:(n>>16)&255, g:(n>>8)&255, b:n&255};
|
|
59223
|
+
}
|
|
59224
|
+
function relLuminance(hex){
|
|
59225
|
+
const {r,g,b}=hexToRgb(hex);
|
|
59226
|
+
const f=v=>{ v/=255; return v<=.03928 ? v/12.92 : Math.pow((v+.055)/1.055,2.4); };
|
|
59227
|
+
return .2126*f(r)+.7152*f(g)+.0722*f(b);
|
|
59228
|
+
}
|
|
59229
|
+
function inkFor(hex){ return relLuminance(hex) < .48 ? "#ffffff" : "#15171c"; }
|
|
59230
|
+
function mixHex(a,b,t){
|
|
59231
|
+
const A=hexToRgb(a), B=hexToRgb(b);
|
|
59232
|
+
const c=k=>Math.round(A[k]+(B[k]-A[k])*t).toString(16).padStart(2,"0");
|
|
59233
|
+
return "#"+c("r")+c("g")+c("b");
|
|
59234
|
+
}
|
|
59235
|
+
function resolveThemeMode(){
|
|
59236
|
+
const m=(library.settings&&library.settings.themeMode)||"auto";
|
|
59237
|
+
if(m==="light"||m==="dark") return m;
|
|
59238
|
+
try{ return (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light"; }
|
|
59239
|
+
catch(e){ return "dark"; }
|
|
59240
|
+
}
|
|
59241
|
+
function applyTheme(){
|
|
59242
|
+
const root=document.documentElement;
|
|
59243
|
+
const custom=(library.settings&&library.settings.custom)||null;
|
|
59244
|
+
root.dataset.theme=resolveThemeMode();
|
|
59245
|
+
const accent=(custom&&custom.accent) || (activeClass()&&activeClass().color) || DEFAULT_ACCENT;
|
|
59246
|
+
root.style.setProperty("--accent", accent);
|
|
59247
|
+
root.style.setProperty("--accent-ink", inkFor(accent));
|
|
59248
|
+
// optional full-palette overrides (background / text)
|
|
59249
|
+
if(custom&&custom.bg) root.style.setProperty("--bg", custom.bg); else root.style.removeProperty("--bg");
|
|
59250
|
+
if(custom&&custom.ink) root.style.setProperty("--ink", custom.ink); else root.style.removeProperty("--ink");
|
|
59251
|
+
if(custom&&custom.surface){
|
|
59252
|
+
root.style.setProperty("--surface", custom.surface);
|
|
59253
|
+
root.style.setProperty("--surface-2", mixHex(custom.surface, custom.ink||"#808080", .10));
|
|
59254
|
+
}else{ root.style.removeProperty("--surface"); root.style.removeProperty("--surface-2"); }
|
|
59255
|
+
// keep the browser chrome colour in step with the background
|
|
59256
|
+
const meta=document.querySelector('meta[name="theme-color"]');
|
|
59257
|
+
if(meta){ const bg=getComputedStyle(root).getPropertyValue("--bg").trim(); if(bg) meta.setAttribute("content", bg); }
|
|
59258
|
+
}
|
|
59259
|
+
function themeColorValue(token){ return (getComputedStyle(document.documentElement).getPropertyValue(token)||"").trim()||"#000000"; }
|
|
59260
|
+
function setThemeMode(m){ library.settings.themeMode=m; saveSettings(); applyTheme(); renderMenu(); }
|
|
59261
|
+
function setCustomColor(key,val){
|
|
59262
|
+
library.settings.custom=library.settings.custom||{};
|
|
59263
|
+
library.settings.custom[key]=val;
|
|
59264
|
+
saveSettings(); applyTheme();
|
|
59265
|
+
}
|
|
59266
|
+
function resetTheme(){ library.settings.custom=null; saveSettings(); applyTheme(); renderMenu(); }
|
|
59071
59267
|
|
|
59072
59268
|
/* ============================================================
|
|
59073
59269
|
CLASS-AWARE HELPERS
|
|
@@ -59130,7 +59326,7 @@ function actionLabel(a){
|
|
|
59130
59326
|
}
|
|
59131
59327
|
const m=String(a).match(/^([1-3])\s*(?:to|or|–|-)\s*([1-3])$/);
|
|
59132
59328
|
if(m) return `${ACTION_GLYPH[m[1]]}–${ACTION_GLYPH[m[2]]} ${m[1]} to ${m[2]} actions`;
|
|
59133
|
-
return
|
|
59329
|
+
return a;
|
|
59134
59330
|
}
|
|
59135
59331
|
function titleCaseTrait(t){ return t.charAt(0).toUpperCase()+t.slice(1); }
|
|
59136
59332
|
function escapeHtml(s){ return (s||"").replace(/[&<>]/g,c=>({"&":"&","<":"<",">":">"}[c])); }
|
|
@@ -59158,12 +59354,11 @@ function scaleDamage(s, castRank){
|
|
|
59158
59354
|
}
|
|
59159
59355
|
function damageChipHTML(s, castRank){
|
|
59160
59356
|
const ds=scaleDamage(s, castRank); if(!ds) return "";
|
|
59161
|
-
|
|
59162
|
-
const
|
|
59357
|
+
return ds.map(d=>{
|
|
59358
|
+
const cls=d.healing?"heal":"dmg";
|
|
59163
59359
|
const ty=(d.type&&!d.healing)?" "+escapeHtml(d.type):"";
|
|
59164
|
-
return
|
|
59165
|
-
});
|
|
59166
|
-
return `<span class="dmgchip">${parts.join(" ")}</span>`;
|
|
59360
|
+
return `<span class="dmgchip ${cls}">${escapeHtml(d.formula)}${ty}</span>`;
|
|
59361
|
+
}).join(" ");
|
|
59167
59362
|
}
|
|
59168
59363
|
|
|
59169
59364
|
function spellCardHTML(s, opts){
|
|
@@ -59176,7 +59371,7 @@ function spellCardHTML(s, opts){
|
|
|
59176
59371
|
if(s.duration) m.push(`<b>Duration</b> ${escapeHtml(s.duration)}`);
|
|
59177
59372
|
const meta = m.length?`<div class="meta">${m.join(" · ")}</div>`:"";
|
|
59178
59373
|
const traits=`<div class="traits">${s.traits.map(t=>`<span class="trait">${escapeHtml(titleCaseTrait(t))}</span>`).join("")}</div>`;
|
|
59179
|
-
const saveTag = s.save ? `<span class="save-tag"
|
|
59374
|
+
const saveTag = s.save ? `<span class="save-tag">${escapeHtml(s.save)}</span>` : "";
|
|
59180
59375
|
const heighten = s.heightened ? `<div class="heighten"><b>Heightened</b> ${textToHtml(s.heightened)}</div>` : "";
|
|
59181
59376
|
const note=legacyNote(s)?`<div class="meta" style="margin:-2px 0 4px"><span class="formerly">${escapeHtml(legacyNote(s))}</span></div>`:"";
|
|
59182
59377
|
return `
|
|
@@ -59199,7 +59394,7 @@ function spellCardHTML(s, opts){
|
|
|
59199
59394
|
function spellPreviewHTML(s, castRank){
|
|
59200
59395
|
if(!s) return "";
|
|
59201
59396
|
const heightNote=(castRank>s.rank)?` · cast at rank ${castRank}`:"";
|
|
59202
|
-
const saveTag=s.save?` ·
|
|
59397
|
+
const saveTag=s.save?` · ${escapeHtml(s.save)}`:"";
|
|
59203
59398
|
const m=[];
|
|
59204
59399
|
if(s.range) m.push(`<b>Range</b> ${escapeHtml(s.range)}`);
|
|
59205
59400
|
if(s.area) m.push(`<b>Area</b> ${escapeHtml(s.area)}`);
|
|
@@ -59251,7 +59446,7 @@ function toast(msg){
|
|
|
59251
59446
|
function charSummary(c){
|
|
59252
59447
|
if(!c.classId) return "New character — no class yet";
|
|
59253
59448
|
const cl=CLASSES[c.classId];
|
|
59254
|
-
return `${cl.
|
|
59449
|
+
return `${cl.name} · Level ${c.level}`;
|
|
59255
59450
|
}
|
|
59256
59451
|
function openMenu(){
|
|
59257
59452
|
VIEWS.forEach(v=>document.getElementById("view-"+v).classList.add("hide"));
|
|
@@ -59268,18 +59463,33 @@ function renderMenu(){
|
|
|
59268
59463
|
const c=library.characters[id];
|
|
59269
59464
|
const active=id===library.activeId;
|
|
59270
59465
|
return `<div class="classcard ${active?"activechar":""}">
|
|
59271
|
-
<div class="ic">${c.classId?CLASSES[c.classId].icon:"
|
|
59466
|
+
<div class="ic" style="color:${c.classId?CLASSES[c.classId].color:"var(--muted)"}">${c.classId?iconSvg(CLASSES[c.classId].icon):iconSvg("plus")}</div>
|
|
59272
59467
|
<div class="txt" onclick="switchCharacter('${id}')" style="cursor:pointer">
|
|
59273
|
-
<div class="nm">${escapeHtml(c.name||(c.classId?CLASSES[c.classId].name:"Unnamed"))}${active?` <span class="previewtag" style="background:var(--
|
|
59468
|
+
<div class="nm">${escapeHtml(c.name||(c.classId?CLASSES[c.classId].name:"Unnamed"))}${active?` <span class="previewtag" style="background:var(--accent);color:var(--accent-ink);border-color:var(--accent)">active</span>`:""}</div>
|
|
59274
59469
|
<div class="tl">${charSummary(c)}</div>
|
|
59275
59470
|
</div>
|
|
59276
59471
|
${ids.length>1?`<button class="rmrow" title="delete" onclick="deleteCharacter('${id}')">✕</button>`:""}
|
|
59277
59472
|
</div>`;
|
|
59278
59473
|
}).join("");
|
|
59279
59474
|
document.getElementById("menuList").innerHTML=list;
|
|
59475
|
+
renderAppearance();
|
|
59280
59476
|
showInstallButton(!!deferredInstall);
|
|
59281
59477
|
const ds=document.getElementById("dataStamp"); if(ds) ds.textContent=dataStampText();
|
|
59282
59478
|
}
|
|
59479
|
+
/* Appearance panel: Light/Dark/Auto + a four-swatch custom palette. */
|
|
59480
|
+
function renderAppearance(){
|
|
59481
|
+
const box=document.getElementById("appearancePanel"); if(!box) return;
|
|
59482
|
+
const mode=(library.settings&&library.settings.themeMode)||"auto";
|
|
59483
|
+
const seg=["light","dark","auto"].map(m=>
|
|
59484
|
+
`<button class="${mode===m?"on":""}" onclick="setThemeMode('${m}')">${m[0].toUpperCase()+m.slice(1)}</button>`).join("");
|
|
59485
|
+
const rows=[["Background","bg","--bg"],["Surface","surface","--surface"],["Text","ink","--ink"],["Accent","accent","--accent"]]
|
|
59486
|
+
.map(([label,key,token])=>`<div class="swatchrow"><label>${label}</label>
|
|
59487
|
+
<input type="color" value="${themeColorValue(token)}" oninput="setCustomColor('${key}',this.value)" aria-label="${label} colour"></div>`).join("");
|
|
59488
|
+
box.innerHTML=`<div class="seg" id="themeSeg">${seg}</div>
|
|
59489
|
+
<p class="meta">Colours follow each class's theme by default. Pick your own below — they apply to every character.</p>
|
|
59490
|
+
${rows}
|
|
59491
|
+
<button class="btn secondary" onclick="resetTheme()">Reset to class default</button>`;
|
|
59492
|
+
}
|
|
59283
59493
|
function newCharacter(){ const c=blankChar(); library.characters[c.id]=c; library.activeId=c.id; state=c; saveState(); showClassPicker(); }
|
|
59284
59494
|
function switchCharacter(id){
|
|
59285
59495
|
if(!library.characters[id]) return;
|
|
@@ -59305,7 +59515,7 @@ function exportCharacter(){
|
|
|
59305
59515
|
const code="PF2E1:"+b64encode(JSON.stringify(c));
|
|
59306
59516
|
const io=document.getElementById("menuIO");
|
|
59307
59517
|
io.value=code; io.focus(); io.select();
|
|
59308
|
-
if(navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(code).then(()=>toast("
|
|
59518
|
+
if(navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(code).then(()=>toast("Copied to clipboard")).catch(()=>toast("Code ready — copy it")); }
|
|
59309
59519
|
else toast("Code ready — copy it");
|
|
59310
59520
|
}
|
|
59311
59521
|
function importCharacter(){
|
|
@@ -59317,7 +59527,7 @@ function importCharacter(){
|
|
|
59317
59527
|
if(typeof obj!=="object"||!("level" in obj)) throw new Error("bad");
|
|
59318
59528
|
const c=Object.assign(blankChar(), obj, {id:uid()});
|
|
59319
59529
|
library.characters[c.id]=c; library.activeId=c.id; state=c; saveState();
|
|
59320
|
-
toast("
|
|
59530
|
+
toast("Character imported");
|
|
59321
59531
|
if(!state.classId){ showClassPicker(); } else { renderHeader(); go(state.prepared?"today":"prepare"); }
|
|
59322
59532
|
}catch(e){ alert("That code didn't work — make sure you pasted the whole thing."); }
|
|
59323
59533
|
}
|
|
@@ -59332,18 +59542,19 @@ function showClassPicker(){
|
|
|
59332
59542
|
const sec=document.getElementById("view-classpick");
|
|
59333
59543
|
sec.classList.remove("hide");
|
|
59334
59544
|
sec.innerHTML=`
|
|
59335
|
-
<h1 style="text-align:center"
|
|
59545
|
+
<h1 style="text-align:center">PF2e Spellbook</h1>
|
|
59336
59546
|
<p class="meta" style="text-align:center;margin-bottom:18px">Choose your class to begin.</p>
|
|
59337
59547
|
${CLASS_ORDER.map(id=>{
|
|
59338
59548
|
const c=CLASSES[id];
|
|
59339
|
-
return `<button class="classcard" onclick="chooseClass('${id}')">
|
|
59340
|
-
<div class="ic">${c.icon}</div>
|
|
59549
|
+
return `<button class="classcard" style="border-left-color:${c.color}" onclick="chooseClass('${id}')">
|
|
59550
|
+
<div class="ic" style="color:${c.color}">${iconSvg(c.icon)}</div>
|
|
59341
59551
|
<div class="txt"><div class="nm">${c.name}${c.preview?` <span class="previewtag">preview</span>`:""}</div>
|
|
59342
59552
|
<div class="tl">${c.tagline}</div></div>
|
|
59343
59553
|
<div class="arr">→</div>
|
|
59344
59554
|
</button>`;
|
|
59345
59555
|
}).join("")}
|
|
59346
59556
|
<p class="meta" style="text-align:center;margin-top:18px;font-size:.8rem">Pathfinder 2e (Remaster) · unofficial fan tool<br>${escapeHtml(dataStampText())}</p>`;
|
|
59557
|
+
applyTheme();
|
|
59347
59558
|
window.scrollTo(0,0);
|
|
59348
59559
|
}
|
|
59349
59560
|
function chooseClass(id){
|
|
@@ -59354,8 +59565,9 @@ function chooseClass(id){
|
|
|
59354
59565
|
if(!hasFeature("divineFont")) state.font="heal";
|
|
59355
59566
|
if(c.traditionFrom==="patron" && !state.patronTradition) state.patronTradition=c.defaultTradition||"occult";
|
|
59356
59567
|
saveState();
|
|
59568
|
+
applyTheme();
|
|
59357
59569
|
renderHeader();
|
|
59358
|
-
toast(`${c.
|
|
59570
|
+
toast(`${c.name} selected`);
|
|
59359
59571
|
go(state.prepared ? "today" : "prepare");
|
|
59360
59572
|
}
|
|
59361
59573
|
function openClassPicker(){ showClassPicker(); }
|
|
@@ -59365,7 +59577,8 @@ function openClassPicker(){ showClassPicker(); }
|
|
|
59365
59577
|
============================================================ */
|
|
59366
59578
|
function renderHeader(){
|
|
59367
59579
|
const c=activeClass(); if(!c) return;
|
|
59368
|
-
|
|
59580
|
+
applyTheme();
|
|
59581
|
+
document.getElementById("charIcon").innerHTML=iconSvg(c.icon);
|
|
59369
59582
|
document.getElementById("charNameHead").textContent=state.name||c.name;
|
|
59370
59583
|
document.getElementById("charSub").textContent=
|
|
59371
59584
|
`${c.name} · Pathfinder 2e (Remaster) · ${TRADITION_LABEL[activeTradition()]} list`;
|
|
@@ -59431,8 +59644,8 @@ function renderCharExtra(){
|
|
|
59431
59644
|
<label class="field"><span class="name">Divine Font</span>
|
|
59432
59645
|
<span class="hint">Which one did you choose at level 1?</span></label>
|
|
59433
59646
|
<div class="seg" id="fontSeg">
|
|
59434
|
-
<button data-font="heal" class="heal ${state.font==="heal"?"on":""}" onclick="setFont('heal')"
|
|
59435
|
-
<button data-font="harm" class="harm ${state.font==="harm"?"on":""}" onclick="setFont('harm')"
|
|
59647
|
+
<button data-font="heal" class="heal ${state.font==="heal"?"on":""}" onclick="setFont('heal')">${iconSvg("heal")} Healing Font</button>
|
|
59648
|
+
<button data-font="harm" class="harm ${state.font==="harm"?"on":""}" onclick="setFont('harm')">${iconSvg("harm")} Harmful Font</button>
|
|
59436
59649
|
</div>`;
|
|
59437
59650
|
}
|
|
59438
59651
|
if(c.traditionFrom==="bloodline"){
|
|
@@ -59463,22 +59676,25 @@ function renderPrepSummary(){
|
|
|
59463
59676
|
let extra="";
|
|
59464
59677
|
if(hasFeature("divineFont")){
|
|
59465
59678
|
const fontName=state.font==="heal"?"Heal":"Harm";
|
|
59466
|
-
extra+=`<div style="margin-top:4px">${state.font==="heal"?"
|
|
59679
|
+
extra+=`<div style="margin-top:4px">${iconSvg(state.font==="heal"?"heal":"harm")} Divine Font: <b>${fontCount()}× ${fontName}</b> (free, heightened to rank ${hr})</div>`;
|
|
59467
59680
|
}
|
|
59468
59681
|
getFeatures("extraSlots").forEach(f=>{
|
|
59469
|
-
extra+=`<div style="margin-top:4px">${f.icon} ${f.label}: <b>+1</b> per rank</div>`;
|
|
59682
|
+
extra+=`<div style="margin-top:4px">${iconSvg(f.icon)} ${f.label}: <b>+1</b> per rank</div>`;
|
|
59470
59683
|
});
|
|
59471
59684
|
getFeatures("dailyResource").forEach(f=>{
|
|
59472
|
-
extra+=`<div style="margin-top:4px">${f.icon} ${f.label}: <b>${f.uses}×</b>/day</div>`;
|
|
59685
|
+
extra+=`<div style="margin-top:4px">${iconSvg(f.icon)} ${f.label}: <b>${f.uses}×</b>/day</div>`;
|
|
59686
|
+
});
|
|
59687
|
+
studiousFeats().forEach(f=>{
|
|
59688
|
+
extra+=`<div style="margin-top:4px">${iconSvg("school")} ${f.label}: <b>${f.count}×</b> rank ${f.rank} (utility)</div>`;
|
|
59473
59689
|
});
|
|
59474
59690
|
if(hasFocus()){
|
|
59475
|
-
extra+=`<div style="margin-top:4px"
|
|
59691
|
+
extra+=`<div style="margin-top:4px">Focus pool: <b>${state.focusPool||1}</b> Focus Point${(state.focusPool||1)>1?"s":""} (focus spells heighten to rank ${focusRank()})</div>`;
|
|
59476
59692
|
}
|
|
59477
59693
|
document.getElementById("prepSummary").innerHTML=`
|
|
59478
59694
|
<div class="meta" style="font-size:1rem">
|
|
59479
|
-
<div
|
|
59480
|
-
<div style="margin-top:8px"
|
|
59481
|
-
<div style="margin-top:4px"
|
|
59695
|
+
<div><b>Spell DC ${spellDC()}</b> · Spell attack ${spellAtk()}</div>
|
|
59696
|
+
<div style="margin-top:8px">Cantrips ${isSpontaneous()?"known":""}: <b>${c.cantrips}</b></div>
|
|
59697
|
+
<div style="margin-top:4px">${isSpontaneous()?"Slots (cast any known spell of that rank)":"Spell slots"}: ${slotText||"—"}</div>
|
|
59482
59698
|
${extra}
|
|
59483
59699
|
</div>`;
|
|
59484
59700
|
}
|
|
@@ -59504,6 +59720,7 @@ function renderPrepDynamic(){
|
|
|
59504
59720
|
getFeatures("note").forEach(f=>{ html+=`<div class="banner"><b>${f.title}</b> — ${f.text}</div>`; });
|
|
59505
59721
|
html+=cantripPicksHTML();
|
|
59506
59722
|
html+= isSpontaneous() ? repertoireHTML() : preparedSlotsHTML();
|
|
59723
|
+
if(!isSpontaneous()) html+=studiousSlotsHTML();
|
|
59507
59724
|
if(hasFocus()) html+=focusPicksHTML();
|
|
59508
59725
|
document.getElementById("prepDynamic").innerHTML=html;
|
|
59509
59726
|
}
|
|
@@ -59522,14 +59739,14 @@ function focusPicksHTML(){
|
|
|
59522
59739
|
<span class="fcname">${escapeHtml(s.name)}</span><span class="fcrank">${rank}</span></label>`;
|
|
59523
59740
|
}).join("");
|
|
59524
59741
|
return `<div class="slotgroup">
|
|
59525
|
-
<h3
|
|
59742
|
+
<h3>Focus spells <span class="count" style="font-weight:600;color:var(--muted)">(pool of ${pool})</span></h3>
|
|
59526
59743
|
<p class="meta">Focus spells cost <b>Focus Points</b> (shared pool, max 3) and auto-heighten to rank ${focusRank()}. <b>Refocus</b> restores points. Check the ones you know — type below to filter to your domain / bloodline / order.</p>
|
|
59527
59744
|
<label class="field" style="margin:8px 0"><span class="name">Focus Point pool</span>
|
|
59528
59745
|
<span class="hint">1 + 1 per extra focus feat, up to 3</span>
|
|
59529
59746
|
<select id="inFocusPool" onchange="setFocusPool(this.value)">
|
|
59530
59747
|
${[1,2,3].map(n=>`<option value="${n}" ${n===pool?"selected":""}>${n} Focus Point${n>1?"s":""}</option>`).join("")}
|
|
59531
59748
|
</select></label>
|
|
59532
|
-
<input type="text" id="focusFilter" placeholder="
|
|
59749
|
+
<input type="text" id="focusFilter" placeholder="Filter focus spells (name or trait)…" oninput="filterFocusChecklist(this.value)" aria-label="filter focus spells">
|
|
59533
59750
|
<div class="focus-checklist" id="focusChecklist">${rows}</div>
|
|
59534
59751
|
</div>`;
|
|
59535
59752
|
}
|
|
@@ -59547,7 +59764,7 @@ function gatherFocus(){
|
|
|
59547
59764
|
function cantripPicksHTML(){
|
|
59548
59765
|
const c=activeClass();
|
|
59549
59766
|
const prev=(state.prepared&&state.prepared.cantrips)||[];
|
|
59550
|
-
let html=`<div class="slotgroup"><h3
|
|
59767
|
+
let html=`<div class="slotgroup"><h3>Cantrips (pick ${c.cantrips})</h3>`;
|
|
59551
59768
|
for(let i=0;i<c.cantrips;i++){
|
|
59552
59769
|
const sel=prev[i]||"", pid="cprev"+i;
|
|
59553
59770
|
html+=`<div class="prep-slot">
|
|
@@ -59570,10 +59787,10 @@ function preparedSlotsHTML(){
|
|
|
59570
59787
|
const fontName=state.font==="heal"?"Heal":"Harm";
|
|
59571
59788
|
const fontSpell=findSpell(state.font==="heal"?feat.healSlug:feat.harmSlug);
|
|
59572
59789
|
html+=`<div class="slotgroup">
|
|
59573
|
-
<h3>${state.font==="heal"?"
|
|
59790
|
+
<h3>${iconSvg(state.font==="heal"?"heal":"harm")} Divine Font — ${fontName} (free slots)</h3>
|
|
59574
59791
|
<p class="meta">These ${fontCount()} castings are free and separate from your normal slots, usable only for <b>${fontName}</b>, auto-heightened to rank ${hr}.</p>`;
|
|
59575
59792
|
for(let i=0;i<fontCount();i++){
|
|
59576
|
-
html+=`<div class="slotrow"><div class="num">${state.font==="heal"?"
|
|
59793
|
+
html+=`<div class="slotrow"><div class="num">${iconSvg(state.font==="heal"?"heal":"harm")}</div>
|
|
59577
59794
|
<select disabled><option>${fontName} (rank ${hr})</option></select></div>`;
|
|
59578
59795
|
}
|
|
59579
59796
|
html+=`<div class="prep-preview">${spellPreviewHTML(fontSpell, hr)}</div></div>`;
|
|
@@ -59583,7 +59800,7 @@ function preparedSlotsHTML(){
|
|
|
59583
59800
|
Object.keys(slots).map(Number).sort((a,b)=>b-a).forEach(r=>{
|
|
59584
59801
|
const n=slots[r];
|
|
59585
59802
|
const prev=prevSlots[r]||[];
|
|
59586
|
-
html+=`<div class="slotgroup"><h3
|
|
59803
|
+
html+=`<div class="slotgroup"><h3>Rank ${r} slots (${n})</h3>`;
|
|
59587
59804
|
for(let i=0;i<n;i++){
|
|
59588
59805
|
const sel=prev[i]||"", pid="sprev"+r+"_"+i;
|
|
59589
59806
|
html+=`<div class="prep-slot">
|
|
@@ -59596,7 +59813,7 @@ function preparedSlotsHTML(){
|
|
|
59596
59813
|
const ex=(prevExtra[f.key]&&prevExtra[f.key][r]&&prevExtra[f.key][r][0])||"";
|
|
59597
59814
|
const pid="xprev"+f.key+r;
|
|
59598
59815
|
html+=`<div class="prep-slot">
|
|
59599
|
-
<div class="slotrow"><div class="num">${f.icon}</div>
|
|
59816
|
+
<div class="slotrow"><div class="num">${iconSvg(f.icon)}</div>
|
|
59600
59817
|
<select class="extraSel" data-key="${f.key}" data-rank="${r}" data-castrank="${r}" data-preview="${pid}" onchange="updatePreview(this)">${spellOptions(r, ex, false)}</select></div>
|
|
59601
59818
|
<div class="prep-preview" id="${pid}">${spellPreviewHTML(findSpell(ex),r)}</div></div>`;
|
|
59602
59819
|
});
|
|
@@ -59605,6 +59822,33 @@ function preparedSlotsHTML(){
|
|
|
59605
59822
|
return html;
|
|
59606
59823
|
}
|
|
59607
59824
|
|
|
59825
|
+
/* Studious spells (magus, level 7+): a couple of extra fixed-rank slots that can
|
|
59826
|
+
only hold a short list of utility spells. Restricted <select> so the app stays
|
|
59827
|
+
honest about what's legal in these slots. */
|
|
59828
|
+
function studiousFeats(){ return getFeatures("studiousSlots").filter(f=>Number(state.level)>=(f.minLevel||1)); }
|
|
59829
|
+
function studiousSlotsHTML(){
|
|
59830
|
+
const feats=studiousFeats(); if(!feats.length) return "";
|
|
59831
|
+
const prevAll=(state.prepared&&state.prepared.studious)||{};
|
|
59832
|
+
let html="";
|
|
59833
|
+
feats.forEach(f=>{
|
|
59834
|
+
const prev=prevAll[f.key]||[];
|
|
59835
|
+
const opts=(f.spells||[]).map(findSpell).filter(Boolean).sort((a,b)=>a.name.localeCompare(b.name));
|
|
59836
|
+
html+=`<div class="slotgroup"><h3>${f.label} <span class="count" style="font-weight:600;color:var(--muted)">(${f.count} × rank ${f.rank}, utility only)</span></h3>
|
|
59837
|
+
<p class="meta">${escapeHtml(f.note||"")}</p>`;
|
|
59838
|
+
for(let i=0;i<f.count;i++){
|
|
59839
|
+
const sel=prev[i]||"", pid="stud_"+f.key+"_"+i;
|
|
59840
|
+
const optionHtml=`<option value="">— choose —</option>`+
|
|
59841
|
+
opts.map(s=>`<option value="${s.slug}" ${s.slug===sel?"selected":""}>${escapeHtml(s.name)}</option>`).join("");
|
|
59842
|
+
html+=`<div class="prep-slot">
|
|
59843
|
+
<div class="slotrow"><div class="num">${iconSvg("school")}</div>
|
|
59844
|
+
<select class="studiousSel" data-key="${f.key}" data-castrank="${f.rank}" data-preview="${pid}" onchange="updatePreview(this)">${optionHtml}</select></div>
|
|
59845
|
+
<div class="prep-preview" id="${pid}">${spellPreviewHTML(findSpell(sel),f.rank)}</div></div>`;
|
|
59846
|
+
}
|
|
59847
|
+
html+=`</div>`;
|
|
59848
|
+
});
|
|
59849
|
+
return html;
|
|
59850
|
+
}
|
|
59851
|
+
|
|
59608
59852
|
let repUID=0;
|
|
59609
59853
|
function repRowHTML(rank, sel, sig){
|
|
59610
59854
|
const pid="rprev"+(repUID++);
|
|
@@ -59628,7 +59872,7 @@ function repertoireHTML(){
|
|
|
59628
59872
|
const count=Math.max(known.length, slots[r]||0);
|
|
59629
59873
|
if(!count) continue; // no slots of this rank (e.g. a summoner's shed low ranks) → no repertoire of that rank
|
|
59630
59874
|
html+=`<div class="slotgroup" data-reprank="${r}">
|
|
59631
|
-
<h3
|
|
59875
|
+
<h3>Rank ${r} spells known <span class="count" style="font-weight:600;color:var(--muted)">(${slots[r]||0} slot${(slots[r]||0)===1?"":"s"}/day)</span></h3>
|
|
59632
59876
|
<div class="repRows" id="repRows-${r}">`;
|
|
59633
59877
|
for(let i=0;i<count;i++){
|
|
59634
59878
|
const e=known[i]||{};
|
|
@@ -59668,8 +59912,11 @@ function savePrep(){
|
|
|
59668
59912
|
document.querySelectorAll(".slotSel").forEach(sel=>{ const r=sel.dataset.rank; (slots[r]=slots[r]||[]).push(sel.value); });
|
|
59669
59913
|
const extra={};
|
|
59670
59914
|
document.querySelectorAll(".extraSel").forEach(sel=>{ const k=sel.dataset.key,r=sel.dataset.rank; extra[k]=extra[k]||{}; (extra[k][r]=extra[k][r]||[]).push(sel.value); });
|
|
59915
|
+
const studious={};
|
|
59916
|
+
document.querySelectorAll(".studiousSel").forEach(sel=>{ const k=sel.dataset.key; (studious[k]=studious[k]||[]).push(sel.value); });
|
|
59671
59917
|
const hr=highestRank(state.level);
|
|
59672
59918
|
state.prepared={ type:"prepared", cantrips, slots, extra };
|
|
59919
|
+
if(Object.keys(studious).length) state.prepared.studious=studious;
|
|
59673
59920
|
if(hasFeature("divineFont")){
|
|
59674
59921
|
const feat=getFeature("divineFont");
|
|
59675
59922
|
state.prepared.divineFont={ font:state.font, fontName:state.font==="heal"?"Heal":"Harm",
|
|
@@ -59678,7 +59925,7 @@ function savePrep(){
|
|
|
59678
59925
|
}
|
|
59679
59926
|
if(focus) state.prepared.focus=focus;
|
|
59680
59927
|
saveState();
|
|
59681
|
-
toast("
|
|
59928
|
+
toast("Prepared for the day!");
|
|
59682
59929
|
go("today");
|
|
59683
59930
|
}
|
|
59684
59931
|
|
|
@@ -59691,7 +59938,7 @@ function todayHeaderHTML(){
|
|
|
59691
59938
|
return `<div class="card" style="text-align:center">
|
|
59692
59939
|
<div style="font-size:1.1rem"><b>${escapeHtml(state.name||c.name)}</b> · ${c.name} · Level ${state.level}</div>
|
|
59693
59940
|
<div class="meta" style="font-size:1rem;margin-top:6px">Spell DC <b>${spellDC()}</b> · Spell attack <b>${spellAtk()}</b></div>
|
|
59694
|
-
<button class="btn secondary" style="margin-top:12px" onclick="newDay()"
|
|
59941
|
+
<button class="btn secondary" style="margin-top:12px" onclick="newDay()">Rest & reset the day</button>
|
|
59695
59942
|
</div>`;
|
|
59696
59943
|
}
|
|
59697
59944
|
function dailyResourceHTML(){
|
|
@@ -59701,14 +59948,34 @@ function dailyResourceHTML(){
|
|
|
59701
59948
|
const spent=state.cast[key]||0, remaining=f.uses-spent, done=remaining<=0;
|
|
59702
59949
|
let pips=""; for(let k=0;k<f.uses;k++){ pips+=`<span class="pip ${k<remaining?"full":"spent"}"></span>`; }
|
|
59703
59950
|
const btn=done?`<button class="castbtn zero" onclick="uncast('${key}')">↩ undo</button>`
|
|
59704
|
-
:`<button class="castbtn" onclick="doCast('${key}',${f.uses})"
|
|
59951
|
+
:`<button class="castbtn" onclick="doCast('${key}',${f.uses})">${f.verb||"Use"}</button>`;
|
|
59705
59952
|
html+=`<div class="cast-card ${done?"spent":""}">
|
|
59706
|
-
<div class="cast-top"><div class="nm">${f.icon} ${f.label}</div><div class="uses">${pips} ${btn}</div></div>
|
|
59953
|
+
<div class="cast-top"><div class="nm">${iconSvg(f.icon)} ${f.label}</div><div class="uses">${pips} ${btn}</div></div>
|
|
59707
59954
|
<div class="meta">${escapeHtml(f.note||"")}</div></div>`;
|
|
59708
59955
|
});
|
|
59709
59956
|
return html;
|
|
59710
59957
|
}
|
|
59711
59958
|
|
|
59959
|
+
/* Escalating-stage tracker (e.g. the oracle's cursebound curse). A simple
|
|
59960
|
+
0..max stepper that persists in state.cast and resets on a new day. */
|
|
59961
|
+
function stageTrackerHTML(){
|
|
59962
|
+
let html="";
|
|
59963
|
+
getFeatures("stageTracker").forEach(f=>{
|
|
59964
|
+
const k="stage:"+f.key;
|
|
59965
|
+
const cur=Math.max(0, Math.min(f.max, state.cast[k]||0));
|
|
59966
|
+
let pips=""; for(let i=1;i<=f.max;i++){ pips+=`<span class="pip ${i<=cur?"full":"spent"}"></span>`; }
|
|
59967
|
+
html+=`<div class="cast-card">
|
|
59968
|
+
<div class="cast-top"><div class="nm">${iconSvg(f.icon)} ${f.label} <span class="meta">stage ${cur} / ${f.max}</span></div>
|
|
59969
|
+
<div class="uses">${pips}
|
|
59970
|
+
<button class="castbtn zero" onclick="stageDown('${f.key}')" ${cur<=0?"disabled":""} aria-label="lower ${escapeHtml(f.label)} stage">−</button>
|
|
59971
|
+
<button class="castbtn" onclick="stageUp('${f.key}')" ${cur>=f.max?"disabled":""} aria-label="raise ${escapeHtml(f.label)} stage">+</button></div></div>
|
|
59972
|
+
<div class="meta">${escapeHtml(f.note||"")}</div></div>`;
|
|
59973
|
+
});
|
|
59974
|
+
return html;
|
|
59975
|
+
}
|
|
59976
|
+
function stageUp(key){ const f=getFeatures("stageTracker").find(x=>x.key===key); if(!f) return; const k="stage:"+key; state.cast[k]=Math.min(f.max,(state.cast[k]||0)+1); saveState(); renderToday(); }
|
|
59977
|
+
function stageDown(key){ const k="stage:"+key; state.cast[k]=Math.max(0,(state.cast[k]||0)-1); saveState(); renderToday(); }
|
|
59978
|
+
|
|
59712
59979
|
function renderToday(){
|
|
59713
59980
|
renderHeader();
|
|
59714
59981
|
const p=state.prepared;
|
|
@@ -59729,41 +59996,58 @@ function focusSectionHTML(p){
|
|
|
59729
59996
|
const fr=focusRank();
|
|
59730
59997
|
let pips=""; for(let k=0;k<pool;k++){ pips+=`<span class="pip ${k<remaining?"full":"spent"}"></span>`; }
|
|
59731
59998
|
const refocus=`<button class="castbtn ${spent>0?"":"zero"}" ${spent>0?"":"disabled"} style="margin-left:8px" onclick="refocus()">↻ Refocus</button>`;
|
|
59732
|
-
let html=`<div class="sectionhead"
|
|
59999
|
+
let html=`<div class="sectionhead">Focus spells <span class="count">${remaining}/${pool} point${pool>1?"s":""}</span></div>
|
|
59733
60000
|
<div style="margin:2px 0 8px"><span class="uses">${pips}${refocus}</span></div><div class="divider"></div>`;
|
|
60001
|
+
const psychic = !!(activeClass() && activeClass().id==="psychic");
|
|
59734
60002
|
f.spells.forEach((slug,i)=>{
|
|
59735
60003
|
const s=findSpell(slug); if(!s) return;
|
|
59736
60004
|
const atWill = s.rank===0;
|
|
59737
60005
|
const out = !atWill && remaining<=0;
|
|
59738
60006
|
const castRank=fr;
|
|
59739
60007
|
const heightNote=(castRank>s.rank)?`<span class="meta"> · ↑ heightened to rank ${castRank}</span>`:"";
|
|
59740
|
-
|
|
59741
|
-
|
|
59742
|
-
|
|
60008
|
+
// Psychic psi cantrips are free at will, but can be Amped by spending a Focus Point.
|
|
60009
|
+
let btn;
|
|
60010
|
+
if(atWill && psychic){
|
|
60011
|
+
const amp = remaining>0
|
|
60012
|
+
? `<button class="castbtn" onclick="castFocus(${pool})" title="Amp — spend 1 Focus Point">Amp (1 pt)</button>`
|
|
60013
|
+
: `<button class="castbtn zero" disabled>No points</button>`;
|
|
60014
|
+
btn = `<span class="unlim">∞ at will</span> ${amp}`;
|
|
60015
|
+
} else if(atWill){
|
|
60016
|
+
btn = `<span class="unlim">∞ at will</span>`;
|
|
60017
|
+
} else {
|
|
60018
|
+
btn = out ? `<button class="castbtn zero" disabled>No points</button>`
|
|
60019
|
+
: `<button class="castbtn" onclick="castFocus(${pool})">Cast</button>`;
|
|
60020
|
+
}
|
|
59743
60021
|
const detailsId="f_"+i;
|
|
59744
|
-
html+=`<div class="cast-card ${out?"spent":""}" style="border-left-color
|
|
59745
|
-
<div class="cast-top"><div class="nm"
|
|
59746
|
-
<div class="meta">${actionLabel(s.actions)}${s.save?` ·
|
|
60022
|
+
html+=`<div class="cast-card ${out?"spent":""}" style="border-left-color:var(--accent)">
|
|
60023
|
+
<div class="cast-top"><div class="nm">${escapeHtml(s.name)} ${heightNote}</div><div class="uses">${btn}</div></div>
|
|
60024
|
+
<div class="meta">${actionLabel(s.actions)}${s.save?` · ${escapeHtml(s.save)}`:""}${s.range?` · ${escapeHtml(s.range)}`:""}</div>${damageChipHTML(s,castRank)?`<div class="dmgline">${damageChipHTML(s,castRank)}</div>`:""}
|
|
59747
60025
|
<button class="detailsbtn" onclick="toggleDetails('${detailsId}',this)">Show details ▾</button>
|
|
59748
60026
|
<div id="${detailsId}" class="hide" style="margin-top:10px">${spellCardHTML(s)}</div></div>`;
|
|
59749
60027
|
});
|
|
59750
60028
|
return html;
|
|
59751
60029
|
}
|
|
59752
60030
|
function castFocus(pool){ const key="focuspool"; state.cast[key]=Math.min(pool,(state.cast[key]||0)+1); saveState(); renderToday(); }
|
|
59753
|
-
function refocus(){
|
|
60031
|
+
function refocus(){
|
|
60032
|
+
const key="focuspool"; state.cast[key]=Math.max(0,(state.cast[key]||0)-1);
|
|
60033
|
+
// Some resources (e.g. the psychic's Unleash Psyche) recharge when you Refocus.
|
|
60034
|
+
getFeatures("dailyResource").forEach(f=>{ if(f.recharge==="refocus") delete state.cast["resource:"+f.key]; });
|
|
60035
|
+
saveState(); renderToday(); toast("Refocused — +1 Focus Point");
|
|
60036
|
+
}
|
|
59754
60037
|
|
|
59755
60038
|
function renderTodayPrepared(p){
|
|
59756
60039
|
let html=todayHeaderHTML();
|
|
59757
60040
|
html+=dailyResourceHTML();
|
|
60041
|
+
html+=stageTrackerHTML();
|
|
59758
60042
|
|
|
59759
60043
|
if(p.divineFont){
|
|
59760
60044
|
const f=p.divineFont; const fontSpell=findSpell(f.fontSlug);
|
|
59761
|
-
html+=`<div class="sectionhead">${f.font==="heal"?"
|
|
60045
|
+
html+=`<div class="sectionhead">${iconSvg(f.font==="heal"?"heal":"harm")} Divine Font — ${f.fontName} <span class="count">free</span></div><div class="divider"></div>`;
|
|
59762
60046
|
html+=castableCard(fontSpell,"font",f.fontRank,f.fontCount,f.fontRank,0,{cls:f.font});
|
|
59763
60047
|
}
|
|
59764
60048
|
|
|
59765
60049
|
if(p.cantrips && p.cantrips.length){
|
|
59766
|
-
html+=`<div class="sectionhead"
|
|
60050
|
+
html+=`<div class="sectionhead">Cantrips <span class="count">unlimited</span></div><div class="divider"></div>`;
|
|
59767
60051
|
p.cantrips.forEach((slug,i)=>{ const s=findSpell(slug); if(!s) return; html+=castableCard(s,"cantrip",0,Infinity,0,i); });
|
|
59768
60052
|
}
|
|
59769
60053
|
|
|
@@ -59773,19 +60057,26 @@ function renderTodayPrepared(p){
|
|
|
59773
60057
|
const extras=[];
|
|
59774
60058
|
extraFeats.forEach(f=>{ const v=p.extra&&p.extra[f.key]&&p.extra[f.key][r]&&p.extra[f.key][r][0]; if(v) extras.push({f,slug:v}); });
|
|
59775
60059
|
if(!arr.length && !extras.length) return;
|
|
59776
|
-
html+=`<div class="sectionhead"
|
|
60060
|
+
html+=`<div class="sectionhead">Rank ${r} spells <span class="count">${arr.length+extras.length} slot${(arr.length+extras.length)>1?"s":""}</span></div><div class="divider"></div>`;
|
|
59777
60061
|
(p.slots[r]||[]).forEach((slug,i)=>{ if(!slug) return; const s=findSpell(slug); if(!s) return; html+=castableCard(s,"slot",r,1,r,i); });
|
|
59778
60062
|
extras.forEach(({f,slug})=>{ const s=findSpell(slug); if(!s) return; html+=castableCard(s,"extra_"+f.key,r,1,r,0,{label:f.icon}); });
|
|
59779
60063
|
});
|
|
60064
|
+
studiousFeats().forEach(f=>{
|
|
60065
|
+
const arr=((p.studious&&p.studious[f.key])||[]).filter(Boolean);
|
|
60066
|
+
if(!arr.length) return;
|
|
60067
|
+
html+=`<div class="sectionhead">${f.label} <span class="count">${arr.length} slot${arr.length>1?"s":""}</span></div><div class="divider"></div>`;
|
|
60068
|
+
arr.forEach((slug,i)=>{ const s=findSpell(slug); if(!s) return; html+=castableCard(s,"studious",f.rank,1,f.rank,i,{label:"school"}); });
|
|
60069
|
+
});
|
|
59780
60070
|
return html;
|
|
59781
60071
|
}
|
|
59782
60072
|
|
|
59783
60073
|
function renderTodaySpontaneous(p){
|
|
59784
60074
|
let html=todayHeaderHTML();
|
|
59785
60075
|
html+=dailyResourceHTML();
|
|
60076
|
+
html+=stageTrackerHTML();
|
|
59786
60077
|
|
|
59787
60078
|
if(p.cantrips && p.cantrips.length){
|
|
59788
|
-
html+=`<div class="sectionhead"
|
|
60079
|
+
html+=`<div class="sectionhead">Cantrips <span class="count">unlimited</span></div><div class="divider"></div>`;
|
|
59789
60080
|
p.cantrips.forEach((slug,i)=>{ const s=findSpell(slug); if(!s) return; html+=castableCard(s,"cantrip",0,Infinity,0,i); });
|
|
59790
60081
|
}
|
|
59791
60082
|
|
|
@@ -59802,7 +60093,7 @@ function renderTodaySpontaneous(p){
|
|
|
59802
60093
|
|
|
59803
60094
|
let pips=""; for(let k=0;k<max;k++){ pips+=`<span class="pip ${k<remaining?"full":"spent"}"></span>`; }
|
|
59804
60095
|
const undo = spent>0 ? `<button class="castbtn zero" style="margin-left:8px" onclick="uncastPool(${R})">↩</button>` : "";
|
|
59805
|
-
html+=`<div class="sectionhead"
|
|
60096
|
+
html+=`<div class="sectionhead">Rank ${R} <span class="count">${remaining}/${max} slot${max>1?"s":""} left</span></div>
|
|
59806
60097
|
<div style="margin:2px 0 8px"><span class="uses">${pips}${undo}</span></div><div class="divider"></div>`;
|
|
59807
60098
|
|
|
59808
60099
|
known.forEach((e,i)=>{ const s=findSpell(e.slug); if(!s) return; html+=poolSpellCard(s,R,R,remaining,max,"k"+i,e.sig); });
|
|
@@ -59822,7 +60113,7 @@ function castableCard(s, kind, rank, uses, castRank, idx, opts){
|
|
|
59822
60113
|
const isSpent=!unlimited && remaining<=0;
|
|
59823
60114
|
const cls = opts.cls || (isSpent?"":cardClass(s));
|
|
59824
60115
|
const heightNote=(castRank>s.rank)?`<span class="meta"> · cast at rank ${castRank}</span>`:"";
|
|
59825
|
-
const tag = opts.label?`<span class="slottag">${opts.label}</span> `:"";
|
|
60116
|
+
const tag = opts.label?`<span class="slottag">${iconSvg(opts.label)}</span> `:"";
|
|
59826
60117
|
|
|
59827
60118
|
let usesHtml="";
|
|
59828
60119
|
if(unlimited){ usesHtml=`<span class="unlim">∞ at will</span>`; }
|
|
@@ -59841,7 +60132,7 @@ function castableCard(s, kind, rank, uses, castRank, idx, opts){
|
|
|
59841
60132
|
<div class="nm">${tag}${escapeHtml(s.name)} ${heightNote}</div>
|
|
59842
60133
|
<div class="uses">${usesHtml} ${btn} ${sustainBtn(s)}</div>
|
|
59843
60134
|
</div>
|
|
59844
|
-
<div class="meta">${actionLabel(s.actions)}${s.save?` ·
|
|
60135
|
+
<div class="meta">${actionLabel(s.actions)}${s.save?` · ${escapeHtml(s.save)}`:""}${s.range?` · ${escapeHtml(s.range)}`:""}</div>${damageChipHTML(s,castRank)?`<div class="dmgline">${damageChipHTML(s,castRank)}</div>`:""}
|
|
59845
60136
|
<button class="detailsbtn" onclick="toggleDetails('${detailsId}',this)">Show details ▾</button>
|
|
59846
60137
|
<div id="${detailsId}" class="hide" style="margin-top:10px">${spellCardHTML(s,{cls})}</div>
|
|
59847
60138
|
</div>`;
|
|
@@ -59862,7 +60153,7 @@ function poolSpellCard(s, poolRank, castRank, remaining, max, uid, sig){
|
|
|
59862
60153
|
<div class="nm">${star}${escapeHtml(s.name)} ${heightNote}</div>
|
|
59863
60154
|
<div class="uses">${btn} ${sustainBtn(s)}</div>
|
|
59864
60155
|
</div>
|
|
59865
|
-
<div class="meta">${actionLabel(s.actions)}${s.save?` ·
|
|
60156
|
+
<div class="meta">${actionLabel(s.actions)}${s.save?` · ${escapeHtml(s.save)}`:""}${s.range?` · ${escapeHtml(s.range)}`:""}</div>${damageChipHTML(s,castRank)?`<div class="dmgline">${damageChipHTML(s,castRank)}</div>`:""}
|
|
59866
60157
|
<button class="detailsbtn" onclick="toggleDetails('${detailsId}',this)">Show details ▾</button>
|
|
59867
60158
|
<div id="${detailsId}" class="hide" style="margin-top:10px">${spellCardHTML(s)}</div>
|
|
59868
60159
|
</div>`;
|
|
@@ -59873,22 +60164,22 @@ function doCast(key,max){ state.cast[key]=(state.cast[key]||0)+1; if(state.cast[
|
|
|
59873
60164
|
function uncast(key){ state.cast[key]=Math.max(0,(state.cast[key]||0)-1); saveState(); renderToday(); }
|
|
59874
60165
|
function castPool(rank,max){ const key="pool:"+rank; state.cast[key]=Math.min(max,(state.cast[key]||0)+1); saveState(); renderToday(); }
|
|
59875
60166
|
function uncastPool(rank){ const key="pool:"+rank; state.cast[key]=Math.max(0,(state.cast[key]||0)-1); saveState(); renderToday(); }
|
|
59876
|
-
function newDay(){ if(confirm("Reset all spent slots for a new day? Your prepared list stays the same.")){ state.cast={}; state.sustaining=[]; saveState(); renderToday(); toast("
|
|
60167
|
+
function newDay(){ if(confirm("Reset all spent slots for a new day? Your prepared list stays the same.")){ state.cast={}; state.sustaining=[]; saveState(); renderToday(); toast("A new day dawns!"); } }
|
|
59877
60168
|
|
|
59878
60169
|
/* ---- Sustained-spell tracker ---- */
|
|
59879
60170
|
function sustainBtn(s){
|
|
59880
60171
|
if(!s || !s.sustained) return "";
|
|
59881
60172
|
const on=(state.sustaining||[]).includes(s.slug);
|
|
59882
|
-
return on ? `<button class="susbtn on" onclick="endSustain('${s.slug}')"
|
|
59883
|
-
: `<button class="susbtn" onclick="startSustain('${s.slug}')"
|
|
60173
|
+
return on ? `<button class="susbtn on" onclick="endSustain('${s.slug}')">End</button>`
|
|
60174
|
+
: `<button class="susbtn" onclick="startSustain('${s.slug}')">Sustain</button>`;
|
|
59884
60175
|
}
|
|
59885
|
-
function startSustain(slug){ state.sustaining=state.sustaining||[]; if(!state.sustaining.includes(slug)){ state.sustaining.push(slug); saveState(); renderToday(); toast("
|
|
60176
|
+
function startSustain(slug){ state.sustaining=state.sustaining||[]; if(!state.sustaining.includes(slug)){ state.sustaining.push(slug); saveState(); renderToday(); toast("Now sustaining"); } }
|
|
59886
60177
|
function endSustain(slug){ state.sustaining=(state.sustaining||[]).filter(x=>x!==slug); saveState(); renderToday(); }
|
|
59887
60178
|
function sustainingBarHTML(){
|
|
59888
60179
|
const list=(state.sustaining||[]).filter(slug=>findSpell(slug));
|
|
59889
60180
|
if(!list.length) return "";
|
|
59890
|
-
return `<div class="card" style="border-left:
|
|
59891
|
-
<div class="sectionhead" style="margin:0 0 6px"
|
|
60181
|
+
return `<div class="card" style="border-left:5px solid var(--info)">
|
|
60182
|
+
<div class="sectionhead" style="margin:0 0 6px">Sustaining now <span class="count">spend an action each turn</span></div>
|
|
59892
60183
|
${list.map(slug=>{const s=findSpell(slug); return `<div class="susrow"><span>${escapeHtml(s.name)}</span><button class="castbtn zero" onclick="endSustain('${slug}')">✓ End</button></div>`;}).join("")}
|
|
59893
60184
|
</div>`;
|
|
59894
60185
|
}
|
|
@@ -59931,8 +60222,8 @@ function renderBrowseFilters(){
|
|
|
59931
60222
|
function renderRankChips(){
|
|
59932
60223
|
const ranks=[["all","All"],["0","Cantrips"]];
|
|
59933
60224
|
for(let r=1;r<=10;r++) ranks.push([String(r),"Rank "+r]);
|
|
59934
|
-
if(hasFocus()) ranks.push(["focus","
|
|
59935
|
-
ranks.push(["rituals","
|
|
60225
|
+
if(hasFocus()) ranks.push(["focus","Focus"]);
|
|
60226
|
+
ranks.push(["rituals","Rituals"]);
|
|
59936
60227
|
document.getElementById("rankChips").innerHTML=
|
|
59937
60228
|
ranks.map(([v,l])=>`<button class="chip ${browseRank===v?"on":""}" onclick="setBrowseRank('${v}')">${l}</button>`).join("");
|
|
59938
60229
|
}
|
|
@@ -59967,7 +60258,7 @@ function renderBrowse(){
|
|
|
59967
60258
|
}
|
|
59968
60259
|
/* Lightweight collapsed row; the full card renders on demand when tapped. */
|
|
59969
60260
|
function browseRowHTML(s){
|
|
59970
|
-
const save=s.save?` ·
|
|
60261
|
+
const save=s.save?` · ${escapeHtml(s.save)}`:"";
|
|
59971
60262
|
const note=legacyNote(s)?` <span class="formerly">${escapeHtml(legacyNote(s))}</span>`:"";
|
|
59972
60263
|
return `<div class="browse-row rank${s.rank}">
|
|
59973
60264
|
<button class="browse-row-head" onclick="toggleBrowse('${s.slug}',this)">
|
|
@@ -60078,6 +60369,14 @@ function downloadOffline(){
|
|
|
60078
60369
|
|
|
60079
60370
|
function init(){
|
|
60080
60371
|
setupInstall();
|
|
60372
|
+
applyTheme();
|
|
60373
|
+
try{
|
|
60374
|
+
if(window.matchMedia){
|
|
60375
|
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{
|
|
60376
|
+
if(((library.settings&&library.settings.themeMode)||"auto")==="auto") applyTheme();
|
|
60377
|
+
});
|
|
60378
|
+
}
|
|
60379
|
+
}catch(e){}
|
|
60081
60380
|
if(!state.classId){ showClassPicker(); }
|
|
60082
60381
|
else { renderHeader(); go(state.prepared?"today":"prepare"); }
|
|
60083
60382
|
}
|