pf2e-spellbook 1.0.0 → 1.1.0
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/dist/icon.svg +3 -3
- package/dist/index.html +561 -269
- package/dist/manifest.webmanifest +2 -2
- package/dist/sw.js +1 -1
- package/package.json +15 -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 +227 -151
- package/src/template.html +31 -16
- package/tools/test.mjs +102 -5
package/dist/index.html
CHANGED
|
@@ -3,168 +3,243 @@
|
|
|
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
|
-
.linklike{background:none;border:none;color:var(--
|
|
198
|
+
.linklike{background:none;border:none;color:var(--info);font-weight:700;text-decoration:underline;padding:0;font-size:inherit;}
|
|
130
199
|
|
|
131
200
|
label.field{display:block;margin:14px 0;}
|
|
132
|
-
label.field .name{display:block;font-weight:700;margin-bottom:6px;font-size:
|
|
201
|
+
label.field .name{display:block;font-weight:700;margin-bottom:6px;font-size:1rem;}
|
|
133
202
|
label.field .hint{display:block;color:var(--muted);font-size:.82rem;font-weight:400;margin-bottom:6px;}
|
|
203
|
+
input,select,textarea{accent-color:var(--accent);}
|
|
134
204
|
input,select{
|
|
135
|
-
width:100%;padding:
|
|
136
|
-
background:var(--
|
|
205
|
+
width:100%;padding:13px;font-size:1.05rem;border-radius:12px;
|
|
206
|
+
background:var(--surface-2);color:var(--ink);border:1px solid var(--line);
|
|
137
207
|
appearance:none;-webkit-appearance:none;
|
|
138
208
|
}
|
|
139
|
-
select{
|
|
209
|
+
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);}
|
|
210
|
+
select{background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);
|
|
140
211
|
background-position:calc(100% - 20px) center,calc(100% - 14px) center;
|
|
141
212
|
background-size:6px 6px,6px 6px;background-repeat:no-repeat;padding-right:40px;}
|
|
213
|
+
input[type="color"]{padding:4px;height:44px;cursor:pointer;}
|
|
142
214
|
.row{display:flex;gap:10px;}
|
|
143
215
|
.row > *{flex:1;}
|
|
144
216
|
|
|
145
217
|
.seg{display:flex;gap:8px;margin:6px 0;}
|
|
146
218
|
.seg button{
|
|
147
|
-
flex:1;padding:
|
|
148
|
-
background:var(--
|
|
219
|
+
flex:1;padding:13px;border-radius:12px;border:1px solid var(--line);
|
|
220
|
+
background:var(--surface-2);color:var(--ink);font-weight:700;
|
|
221
|
+
display:inline-flex;align-items:center;justify-content:center;gap:8px;
|
|
149
222
|
}
|
|
150
|
-
.seg button.on
|
|
151
|
-
.seg button.on.
|
|
223
|
+
.seg button.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
|
|
224
|
+
.seg button.on.heal{background:var(--heal);color:#08140d;border-color:var(--heal);}
|
|
225
|
+
.seg button.on.harm{background:var(--harm);color:#1c0709;border-color:var(--harm);}
|
|
152
226
|
|
|
153
227
|
/* ---- Slot picker rows ---- */
|
|
154
228
|
.slotgroup{margin:18px 0;}
|
|
155
|
-
.slotgroup h3{color:var(--
|
|
229
|
+
.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
230
|
.slotrow{display:flex;align-items:center;gap:10px;margin:10px 0;}
|
|
157
231
|
.slotrow .num{
|
|
158
232
|
width:34px;height:34px;flex:0 0 34px;border-radius:50%;
|
|
159
|
-
background:var(--
|
|
233
|
+
background:var(--surface-2);border:1px solid var(--accent-dim);color:var(--accent-text);
|
|
160
234
|
display:flex;align-items:center;justify-content:center;font-weight:800;
|
|
161
235
|
}
|
|
236
|
+
.slotrow .num .icn{width:1.1rem;height:1.1rem;}
|
|
162
237
|
.slotrow select{flex:1;}
|
|
163
238
|
.prep-slot{margin:10px 0;}
|
|
164
239
|
.prep-slot .slotrow{margin:0;}
|
|
165
240
|
.prep-preview{margin:6px 0 0 44px;}
|
|
166
241
|
.prep-preview-inner{
|
|
167
|
-
background:var(--
|
|
242
|
+
background:var(--surface-2);border:1px solid var(--line);border-radius:10px;
|
|
168
243
|
padding:10px 12px;
|
|
169
244
|
}
|
|
170
245
|
.prep-preview .meta{font-size:.85rem;margin:2px 0;}
|
|
@@ -173,123 +248,128 @@ select{background-image:linear-gradient(45deg,transparent 50%,var(--gold) 50%),l
|
|
|
173
248
|
|
|
174
249
|
/* ---- Today cast cards ---- */
|
|
175
250
|
.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(--
|
|
251
|
+
background:var(--surface);border:1px solid var(--line);border-radius:14px;
|
|
252
|
+
padding:14px;margin:12px 0;border-left:5px solid var(--line);box-shadow:var(--shadow);
|
|
253
|
+
}
|
|
254
|
+
.cast-card.rank0{border-left-color:var(--rank0);}
|
|
255
|
+
.cast-card.rank1{border-left-color:var(--rank1);}
|
|
256
|
+
.cast-card.rank2{border-left-color:var(--rank2);}
|
|
257
|
+
.cast-card.rank3{border-left-color:var(--rank3);}
|
|
258
|
+
.cast-card.rank4{border-left-color:var(--rank4);}
|
|
259
|
+
.cast-card.rank5{border-left-color:var(--rank5);}
|
|
260
|
+
.cast-card.rank6{border-left-color:var(--rank6);}
|
|
261
|
+
.cast-card.rank7{border-left-color:var(--rank7);}
|
|
262
|
+
.cast-card.rank8{border-left-color:var(--rank8);}
|
|
263
|
+
.cast-card.rank9{border-left-color:var(--rank9);}
|
|
264
|
+
.cast-card.rank10{border-left-color:var(--rank10);}
|
|
190
265
|
.cast-card.heal{border-left-color:var(--heal);}
|
|
191
266
|
.cast-card.harm{border-left-color:var(--harm);}
|
|
192
|
-
.cast-card.spent{opacity:.
|
|
267
|
+
.cast-card.spent{opacity:.45;}
|
|
193
268
|
.cast-top{display:flex;justify-content:space-between;align-items:center;gap:10px;}
|
|
194
|
-
.cast-top .nm{font-size:1.
|
|
269
|
+
.cast-top .nm{font-size:1.15rem;font-weight:700;}
|
|
195
270
|
.uses{display:flex;gap:6px;align-items:center;}
|
|
196
|
-
.pip{width:
|
|
197
|
-
.pip.full{background:var(--
|
|
271
|
+
.pip{width:24px;height:24px;border-radius:50%;border:2px solid var(--accent);display:inline-block;}
|
|
272
|
+
.pip.full{background:var(--accent);}
|
|
198
273
|
.pip.spent{background:transparent;opacity:.35;}
|
|
199
274
|
.castbtn{
|
|
200
|
-
padding:10px 16px;border-radius:10px;border:none;background:var(--
|
|
201
|
-
color
|
|
275
|
+
padding:10px 16px;border-radius:10px;border:none;background:var(--accent);
|
|
276
|
+
color:var(--accent-ink);font-weight:800;font-size:1rem;
|
|
202
277
|
}
|
|
203
|
-
.castbtn.zero{background:var(--
|
|
278
|
+
.castbtn.zero{background:var(--surface-2);color:var(--muted);}
|
|
204
279
|
.unlim{font-size:.85rem;color:var(--heal);font-weight:700;}
|
|
205
280
|
.detailsbtn{
|
|
206
|
-
background:none;border:none;color:var(--
|
|
281
|
+
background:none;border:none;color:var(--info);font-weight:700;
|
|
207
282
|
padding:8px 0 0;font-size:.95rem;text-decoration:underline;
|
|
208
283
|
}
|
|
209
284
|
|
|
210
285
|
.susbtn{
|
|
211
|
-
padding:10px 14px;border-radius:10px;border:1px solid var(--
|
|
212
|
-
background
|
|
286
|
+
padding:10px 14px;border-radius:10px;border:1px solid var(--info);
|
|
287
|
+
background:var(--info-bg);color:var(--info-ink);font-weight:800;font-size:.95rem;
|
|
213
288
|
}
|
|
214
|
-
.susbtn.on{background:var(--
|
|
289
|
+
.susbtn.on{background:var(--info);color:var(--surface);}
|
|
215
290
|
.susrow{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:6px 0;border-top:1px solid var(--line);}
|
|
216
291
|
.susrow:first-of-type{border-top:none;}
|
|
217
292
|
.susrow span{font-weight:700;}
|
|
218
293
|
|
|
219
294
|
.dmgline{margin:4px 0 2px;}
|
|
220
295
|
.dmgchip{
|
|
221
|
-
display:inline-block;background
|
|
222
|
-
color:var(--
|
|
296
|
+
display:inline-block;background:var(--surface-2);border:1px solid var(--line);
|
|
297
|
+
color:var(--ink);font-weight:800;font-size:.92rem;border-radius:8px;padding:2px 10px;
|
|
223
298
|
}
|
|
299
|
+
.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);}
|
|
300
|
+
.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
301
|
|
|
225
302
|
.empty{text-align:center;color:var(--muted);padding:40px 20px;}
|
|
226
303
|
|
|
227
304
|
.filterbar{display:flex;gap:8px;margin:6px 0 14px;flex-wrap:wrap;}
|
|
228
305
|
.chip{
|
|
229
|
-
padding:8px 14px;border-radius:20px;border:1px solid var(--
|
|
230
|
-
background:var(--
|
|
306
|
+
padding:8px 14px;border-radius:20px;border:1px solid var(--line);
|
|
307
|
+
background:var(--surface-2);color:var(--muted);font-weight:700;font-size:.88rem;
|
|
308
|
+
display:inline-flex;align-items:center;gap:6px;
|
|
231
309
|
}
|
|
232
|
-
.chip.on{background:var(--
|
|
310
|
+
.chip.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
|
|
233
311
|
#search{margin-bottom:8px;}
|
|
234
312
|
#filterToggle{margin:0 0 8px;}
|
|
235
|
-
#browseFilters{background:var(--
|
|
313
|
+
#browseFilters{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin:0 0 12px;}
|
|
236
314
|
.fgroup{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:4px 0;}
|
|
237
315
|
.fgroup .flabel{font-size:.8rem;color:var(--muted);font-weight:700;min-width:58px;}
|
|
238
316
|
|
|
239
317
|
.sectionhead{
|
|
240
|
-
color:var(--
|
|
318
|
+
color:var(--accent-text);font-size:1.1rem;font-weight:800;margin:22px 0 4px;
|
|
241
319
|
display:flex;align-items:center;gap:10px;
|
|
242
320
|
}
|
|
243
|
-
.sectionhead .count{font-size:.
|
|
321
|
+
.sectionhead .count{font-size:.82rem;color:var(--muted);font-weight:600;}
|
|
244
322
|
.divider{height:1px;background:var(--line);margin:4px 0 6px;}
|
|
245
323
|
|
|
246
324
|
.banner{
|
|
247
|
-
background
|
|
248
|
-
padding:12px 14px;margin:12px 0;color:var(--
|
|
325
|
+
background:var(--accent-soft);border:1px solid var(--accent-dim);border-radius:12px;
|
|
326
|
+
padding:12px 14px;margin:12px 0;color:var(--ink);font-size:.9rem;
|
|
249
327
|
}
|
|
250
|
-
.banner b{color:var(--
|
|
328
|
+
.banner b{color:var(--accent-text);}
|
|
251
329
|
|
|
252
|
-
details.help{background:var(--
|
|
253
|
-
details.help summary{padding:16px 0;font-weight:700;font-size:1.
|
|
330
|
+
details.help{background:var(--surface);border:1px solid var(--line);border-radius:14px;margin:12px 0;padding:0 16px;}
|
|
331
|
+
details.help summary{padding:16px 0;font-weight:700;font-size:1.05rem;color:var(--accent-text);cursor:pointer;list-style:none;}
|
|
254
332
|
details.help summary::-webkit-details-marker{display:none;}
|
|
255
333
|
details.help summary::after{content:"+";float:right;color:var(--muted);}
|
|
256
334
|
details.help[open] summary::after{content:"-";}
|
|
257
335
|
details.help .body{padding:0 0 16px;font-size:1rem;line-height:1.6;color:var(--ink);}
|
|
258
|
-
details.help .body b{color:var(--
|
|
336
|
+
details.help .body b{color:var(--accent-text);}
|
|
259
337
|
|
|
260
338
|
/* ---- Class picker ---- */
|
|
261
339
|
.classcard{
|
|
262
340
|
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);
|
|
341
|
+
background:var(--surface);border:1px solid var(--line);border-left:4px solid var(--accent);
|
|
342
|
+
border-radius:var(--radius);padding:16px;margin:12px 0;color:var(--ink);box-shadow:var(--shadow);
|
|
265
343
|
}
|
|
266
344
|
.classcard:active{transform:translateY(1px);}
|
|
267
|
-
.classcard .ic{font-size:
|
|
345
|
+
.classcard .ic{font-size:2rem;line-height:1;display:inline-flex;color:var(--accent-text);}
|
|
268
346
|
.classcard .txt{flex:1;}
|
|
269
|
-
.classcard .nm{font-size:1.
|
|
270
|
-
.classcard .tl{font-size:.
|
|
271
|
-
.classcard .arr{color:var(--
|
|
347
|
+
.classcard .nm{font-size:1.25rem;font-weight:800;color:var(--ink);}
|
|
348
|
+
.classcard .tl{font-size:.88rem;color:var(--muted);margin-top:2px;}
|
|
349
|
+
.classcard .arr{color:var(--muted);font-size:1.3rem;font-weight:800;}
|
|
272
350
|
.previewtag{
|
|
273
351
|
font-size:.62rem;text-transform:uppercase;letter-spacing:.5px;vertical-align:middle;
|
|
274
|
-
background
|
|
352
|
+
background:color-mix(in srgb, var(--harm) 15%, var(--surface));
|
|
353
|
+
border:1px solid color-mix(in srgb, var(--harm) 40%, var(--line));
|
|
354
|
+
color:var(--harm);border-radius:6px;padding:1px 6px;font-weight:700;
|
|
275
355
|
}
|
|
276
356
|
|
|
277
357
|
/* ---- Browse compact rows (tap to expand) ---- */
|
|
278
358
|
.browse-row{
|
|
279
|
-
background:var(--
|
|
359
|
+
background:var(--surface);border:1px solid var(--line);border-left:4px solid var(--line);
|
|
280
360
|
border-radius:12px;margin:8px 0;overflow:hidden;
|
|
281
361
|
}
|
|
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(--
|
|
362
|
+
.browse-row.rank0{border-left-color:var(--rank0);}
|
|
363
|
+
.browse-row.rank1{border-left-color:var(--rank1);}
|
|
364
|
+
.browse-row.rank2{border-left-color:var(--rank2);}
|
|
365
|
+
.browse-row.rank3{border-left-color:var(--rank3);}
|
|
366
|
+
.browse-row.rank4{border-left-color:var(--rank4);}
|
|
367
|
+
.browse-row.rank5{border-left-color:var(--rank5);}
|
|
368
|
+
.browse-row.rank6{border-left-color:var(--rank6);}
|
|
369
|
+
.browse-row.rank7{border-left-color:var(--rank7);}
|
|
370
|
+
.browse-row.rank8{border-left-color:var(--rank8);}
|
|
371
|
+
.browse-row.rank9{border-left-color:var(--rank9);}
|
|
372
|
+
.browse-row.rank10{border-left-color:var(--rank10);}
|
|
293
373
|
.browse-row-head{
|
|
294
374
|
display:flex;flex-direction:column;align-items:flex-start;gap:2px;
|
|
295
375
|
width:100%;text-align:left;background:none;border:none;color:var(--ink);
|
|
@@ -297,75 +377,83 @@ details.help .body b{color:var(--gold);}
|
|
|
297
377
|
}
|
|
298
378
|
.browse-row-head::after{content:"▾";position:absolute;right:14px;top:14px;color:var(--muted);}
|
|
299
379
|
.browse-row-head.open::after{content:"▴";}
|
|
300
|
-
.browse-row-head .brn{font-size:1.
|
|
380
|
+
.browse-row-head .brn{font-size:1.05rem;font-weight:700;}
|
|
301
381
|
.browse-row-head .brmeta{font-size:.82rem;color:var(--muted);}
|
|
302
|
-
.brfocus{font-size:.62rem;text-transform:uppercase;letter-spacing:.5px;color
|
|
382
|
+
.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
383
|
.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
|
|
384
|
+
.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
385
|
.browse-card{padding:0 12px 4px;}
|
|
306
386
|
.browse-card .card{margin:0 0 10px;}
|
|
307
387
|
|
|
308
388
|
/* ---- Header menu button + character menu ---- */
|
|
309
389
|
.tophead{display:flex;align-items:center;justify-content:space-between;gap:10px;}
|
|
310
390
|
.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(--
|
|
391
|
+
flex:0 0 auto;background:var(--surface-2);border:1px solid var(--line);
|
|
392
|
+
border-radius:12px;width:44px;height:44px;font-size:1.3rem;line-height:1;color:var(--accent-text);
|
|
393
|
+
display:inline-flex;align-items:center;justify-content:center;
|
|
313
394
|
}
|
|
314
|
-
.classcard.activechar{border-left-color:var(--
|
|
395
|
+
.classcard.activechar{border-left-color:var(--accent);background:var(--accent-soft);}
|
|
315
396
|
textarea{
|
|
316
|
-
width:100%;padding:
|
|
317
|
-
background:var(--
|
|
397
|
+
width:100%;padding:13px;font-size:1rem;border-radius:12px;line-height:1.4;
|
|
398
|
+
background:var(--surface-2);color:var(--ink);border:1px solid var(--line);
|
|
318
399
|
font-family:inherit;resize:vertical;margin:6px 0;
|
|
319
400
|
}
|
|
320
401
|
|
|
402
|
+
/* ---- Appearance / custom palette ---- */
|
|
403
|
+
.swatchrow{display:flex;align-items:center;gap:10px;margin:8px 0;}
|
|
404
|
+
.swatchrow label{flex:1;font-weight:600;font-size:.95rem;}
|
|
405
|
+
.swatchrow input[type="color"]{flex:0 0 56px;width:56px;}
|
|
406
|
+
|
|
321
407
|
/* ---- Repertoire builder (spontaneous) ---- */
|
|
322
408
|
.rep-row .slotrow{gap:8px;}
|
|
323
409
|
.sigstar{
|
|
324
410
|
width:38px;height:38px;flex:0 0 38px;border-radius:10px;
|
|
325
|
-
background:var(--
|
|
411
|
+
background:var(--surface-2);border:1px solid var(--accent-dim);color:var(--muted);
|
|
326
412
|
font-size:1.2rem;line-height:1;display:flex;align-items:center;justify-content:center;
|
|
327
413
|
}
|
|
328
|
-
.sigstar.on{background:var(--
|
|
414
|
+
.sigstar.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
|
|
329
415
|
.rmrow{
|
|
330
416
|
width:38px;height:38px;flex:0 0 38px;border-radius:10px;
|
|
331
|
-
background:var(--
|
|
417
|
+
background:var(--surface-2);border:1px solid var(--line);color:var(--muted);font-weight:700;
|
|
332
418
|
}
|
|
333
|
-
.focus-checklist{max-height:320px;overflow-y:auto;border:1px solid var(--line);border-radius:12px;margin-top:8px;background:var(--
|
|
419
|
+
.focus-checklist{max-height:320px;overflow-y:auto;border:1px solid var(--line);border-radius:12px;margin-top:8px;background:var(--surface-2);}
|
|
334
420
|
.fcheck{display:flex;align-items:center;gap:10px;padding:11px 12px;border-bottom:1px solid var(--line);cursor:pointer;}
|
|
335
421
|
.fcheck:last-child{border-bottom:none;}
|
|
336
422
|
.fcheck input{width:22px;height:22px;flex:0 0 22px;}
|
|
337
423
|
.fcheck .fcname{flex:1;font-weight:600;}
|
|
338
|
-
.fcheck .fcrank{font-size:.75rem;color:var(--muted);background:var(--
|
|
424
|
+
.fcheck .fcrank{font-size:.75rem;color:var(--muted);background:var(--surface);border:1px solid var(--line);border-radius:6px;padding:1px 7px;}
|
|
339
425
|
|
|
340
426
|
.btn.addrow{
|
|
341
427
|
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(--
|
|
428
|
+
font-size:.95rem;font-weight:700;border-radius:10px;box-shadow:none;
|
|
429
|
+
background:var(--surface-2);color:var(--accent-text);border:1px solid var(--accent-dim);
|
|
344
430
|
}
|
|
345
431
|
.slottag{
|
|
346
|
-
|
|
347
|
-
|
|
432
|
+
display:inline-flex;align-items:center;gap:4px;
|
|
433
|
+
font-size:.85rem;background:var(--surface-2);border:1px solid var(--accent-dim);
|
|
434
|
+
color:var(--accent-text);border-radius:6px;padding:1px 6px;margin-right:4px;
|
|
348
435
|
}
|
|
436
|
+
.slottag .icn{width:.95rem;height:.95rem;}
|
|
349
437
|
|
|
350
438
|
/* ---- Bottom nav ---- */
|
|
351
439
|
nav.bottom{
|
|
352
440
|
position:fixed;bottom:0;left:0;right:0;z-index:30;
|
|
353
|
-
background
|
|
441
|
+
background:var(--surface);border-top:1px solid var(--line);
|
|
354
442
|
display:flex;justify-content:space-around;
|
|
355
443
|
padding:6px 4px calc(6px + env(safe-area-inset-bottom));
|
|
356
444
|
}
|
|
357
445
|
nav.bottom button{
|
|
358
446
|
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:.
|
|
447
|
+
display:flex;flex-direction:column;align-items:center;gap:3px;
|
|
448
|
+
padding:8px 2px;font-size:.7rem;font-weight:700;
|
|
361
449
|
}
|
|
362
|
-
nav.bottom button .ic{font-size:1.
|
|
363
|
-
nav.bottom button.on{color:var(--
|
|
450
|
+
nav.bottom button .ic{font-size:1.4rem;line-height:1;display:inline-flex;}
|
|
451
|
+
nav.bottom button.on{color:var(--accent-text);}
|
|
364
452
|
.toast{
|
|
365
453
|
position:fixed;bottom:96px;left:50%;transform:translateX(-50%);
|
|
366
|
-
background:var(--
|
|
454
|
+
background:var(--accent);color:var(--accent-ink);font-weight:800;padding:12px 20px;
|
|
367
455
|
border-radius:30px;z-index:50;opacity:0;transition:opacity .25s;pointer-events:none;
|
|
368
|
-
box-shadow:0 4px 16px rgba(0,0,0,.
|
|
456
|
+
box-shadow:0 4px 16px rgba(0,0,0,.35);
|
|
369
457
|
}
|
|
370
458
|
.toast.show{opacity:1;}
|
|
371
459
|
|
|
@@ -375,8 +463,8 @@ nav.bottom button.on{color:var(--gold);}
|
|
|
375
463
|
|
|
376
464
|
<header class="top hide">
|
|
377
465
|
<div class="tophead">
|
|
378
|
-
<h1><span id="charIcon"
|
|
379
|
-
<button class="menubtn" onclick="openMenu()" title="Characters & backup" aria-label="Characters menu"
|
|
466
|
+
<h1><span id="charIcon"></span> <span id="charNameHead">Spellbook</span></h1>
|
|
467
|
+
<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
468
|
</div>
|
|
381
469
|
<div class="sub" id="charSub">Pathfinder 2e (Remaster)</div>
|
|
382
470
|
<div class="statbar" id="statbar">
|
|
@@ -391,11 +479,14 @@ nav.bottom button.on{color:var(--gold);}
|
|
|
391
479
|
|
|
392
480
|
<!-- ================= CHARACTER MENU ================= -->
|
|
393
481
|
<section id="view-menu" class="wrap hide">
|
|
394
|
-
<h2
|
|
482
|
+
<h2>Appearance</h2>
|
|
483
|
+
<div id="appearancePanel"></div>
|
|
484
|
+
|
|
485
|
+
<h2 style="margin-top:24px">Use offline</h2>
|
|
395
486
|
<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
487
|
<div class="row">
|
|
397
|
-
<button class="btn" onclick="downloadOffline()"
|
|
398
|
-
<button class="btn secondary" id="installBtn" style="display:none" onclick="installApp()"
|
|
488
|
+
<button class="btn" onclick="downloadOffline()">Save offline copy</button>
|
|
489
|
+
<button class="btn secondary" id="installBtn" style="display:none" onclick="installApp()">Install app</button>
|
|
399
490
|
</div>
|
|
400
491
|
<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
492
|
|
|
@@ -407,8 +498,8 @@ nav.bottom button.on{color:var(--gold);}
|
|
|
407
498
|
<h2 style="margin-top:24px">Share & back up</h2>
|
|
408
499
|
<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
500
|
<div class="row">
|
|
410
|
-
<button class="btn secondary" onclick="exportCharacter()"
|
|
411
|
-
<button class="btn secondary" onclick="importCharacter()"
|
|
501
|
+
<button class="btn secondary" onclick="exportCharacter()">Export active</button>
|
|
502
|
+
<button class="btn secondary" onclick="importCharacter()">Import code</button>
|
|
412
503
|
</div>
|
|
413
504
|
<textarea id="menuIO" rows="3" placeholder="Character code appears here on Export — or paste one here, then tap Import."></textarea>
|
|
414
505
|
<button class="btn secondary" onclick="closeMenu()">← Back</button>
|
|
@@ -428,7 +519,7 @@ nav.bottom button.on{color:var(--gold);}
|
|
|
428
519
|
<!-- ================= PREPARE / REPERTOIRE ================= -->
|
|
429
520
|
<section id="view-prepare" class="wrap hide">
|
|
430
521
|
<p class="meta" id="prepClassLine"></p>
|
|
431
|
-
<div class="banner hide" id="prepPreviewBanner"
|
|
522
|
+
<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
523
|
|
|
433
524
|
<h2>① Your character</h2>
|
|
434
525
|
<p class="meta">Copy these from your character sheet.</p>
|
|
@@ -465,9 +556,9 @@ nav.bottom button.on{color:var(--gold);}
|
|
|
465
556
|
<!-- ================= BROWSE ================= -->
|
|
466
557
|
<section id="view-browse" class="wrap hide">
|
|
467
558
|
<h2 id="browseTitle">All spells</h2>
|
|
468
|
-
<input type="text" id="search" placeholder="
|
|
559
|
+
<input type="text" id="search" placeholder="Search spells…" oninput="renderBrowse()">
|
|
469
560
|
<div class="filterbar" id="rankChips"></div>
|
|
470
|
-
<button class="chip" id="filterToggle" onclick="toggleBrowseFilters()" aria-expanded="false"
|
|
561
|
+
<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
562
|
<div id="browseFilters" class="hide"></div>
|
|
472
563
|
<div id="browseList"></div>
|
|
473
564
|
</section>
|
|
@@ -482,10 +573,10 @@ nav.bottom button.on{color:var(--gold);}
|
|
|
482
573
|
<div class="toast" id="toast"></div>
|
|
483
574
|
|
|
484
575
|
<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"
|
|
576
|
+
<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>
|
|
577
|
+
<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>
|
|
578
|
+
<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>
|
|
579
|
+
<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
580
|
</nav>
|
|
490
581
|
|
|
491
582
|
<script>
|
|
@@ -58710,6 +58801,46 @@ function psychicSlots(level){
|
|
|
58710
58801
|
return slots;
|
|
58711
58802
|
}
|
|
58712
58803
|
|
|
58804
|
+
/* ============================================================
|
|
58805
|
+
INLINE SVG ICON REGISTRY
|
|
58806
|
+
Thin line icons (24x24, stroke = currentColor) so they inherit the
|
|
58807
|
+
active accent colour. Keyed by name; classes/features reference a key
|
|
58808
|
+
instead of an emoji. iconSvg() wraps the path data in an <svg>.
|
|
58809
|
+
============================================================ */
|
|
58810
|
+
const ICONS={
|
|
58811
|
+
/* classes */
|
|
58812
|
+
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"/>',
|
|
58813
|
+
wizard:'<path d="M12 3.5 4.5 19.5h15z"/><circle cx="12" cy="13.5" r="1.9"/>',
|
|
58814
|
+
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"/>',
|
|
58815
|
+
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"/>',
|
|
58816
|
+
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"/>',
|
|
58817
|
+
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"/>',
|
|
58818
|
+
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"/>',
|
|
58819
|
+
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"/>',
|
|
58820
|
+
summoner:'<circle cx="9.2" cy="12" r="4.8"/><circle cx="14.8" cy="12" r="4.8"/>',
|
|
58821
|
+
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"/>',
|
|
58822
|
+
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"/>',
|
|
58823
|
+
/* navigation */
|
|
58824
|
+
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"/>',
|
|
58825
|
+
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"/>',
|
|
58826
|
+
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"/>',
|
|
58827
|
+
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"/>',
|
|
58828
|
+
/* features / utility */
|
|
58829
|
+
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"/>',
|
|
58830
|
+
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"/>',
|
|
58831
|
+
heal:'<path d="M12 5.5v13M5.5 12h13"/>',
|
|
58832
|
+
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"/>',
|
|
58833
|
+
menu:'<circle cx="12" cy="8" r="3.4"/><path d="M5.5 20a6.5 6.5 0 0 1 13 0"/>',
|
|
58834
|
+
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"/>',
|
|
58835
|
+
plus:'<path d="M12 5.5v13M5.5 12h13"/>',
|
|
58836
|
+
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"/>',
|
|
58837
|
+
};
|
|
58838
|
+
function iconSvg(name, cls){
|
|
58839
|
+
return '<svg class="icn '+(cls||"")+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" '+
|
|
58840
|
+
'stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'+
|
|
58841
|
+
(ICONS[name]||"")+'</svg>';
|
|
58842
|
+
}
|
|
58843
|
+
|
|
58713
58844
|
/* [minLevel, proficiency bonus] for spell DC / spell attack, highest first.
|
|
58714
58845
|
Bonus = 2/4/6/8 for trained/expert/master/legendary.
|
|
58715
58846
|
Wizard, Druid, Sorcerer, Bard, Oracle, Psychic and the Cloistered Cleric
|
|
@@ -58757,13 +58888,13 @@ const CORE_HELP=[
|
|
|
58757
58888
|
["Remaster vs. legacy (original) rules",
|
|
58758
58889
|
"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
58890
|
["Multiple characters, backup & sharing",
|
|
58760
|
-
"Tap the <b
|
|
58891
|
+
"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
58892
|
];
|
|
58762
58893
|
|
|
58763
58894
|
const CLASSES={
|
|
58764
58895
|
/* ---------------- CLERIC ---------------- */
|
|
58765
58896
|
cleric:{
|
|
58766
|
-
id:"cleric", name:"Cleric", icon:"
|
|
58897
|
+
id:"cleric", name:"Cleric", icon:"cleric", color:"#d8a23a",
|
|
58767
58898
|
tagline:"Divine prepared caster · the party's main healer",
|
|
58768
58899
|
tradition:"divine", keyAbility:"Wisdom", keyAbilityShort:"Wis",
|
|
58769
58900
|
casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
|
|
@@ -58786,63 +58917,59 @@ const CLASSES={
|
|
|
58786
58917
|
|
|
58787
58918
|
/* ---------------- WIZARD ---------------- */
|
|
58788
58919
|
wizard:{
|
|
58789
|
-
id:"wizard", name:"Wizard", icon:"
|
|
58920
|
+
id:"wizard", name:"Wizard", icon:"wizard", color:"#7b6cf0",
|
|
58790
58921
|
tagline:"Arcane prepared caster · the widest spell list",
|
|
58791
58922
|
tradition:"arcane", keyAbility:"Intelligence", keyAbilityShort:"Int",
|
|
58792
58923
|
casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
|
|
58793
|
-
preview:true,
|
|
58794
58924
|
features:[
|
|
58795
|
-
{type:"extraSlots", key:"school", icon:"
|
|
58925
|
+
{type:"extraSlots", key:"school", icon:"school", label:"Arcane school slot",
|
|
58796
58926
|
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:"
|
|
58927
|
+
{type:"dailyResource", key:"bonded", icon:"bonded", label:"Drain Bonded Item", uses:1,
|
|
58798
58928
|
note:"Once per day, recast a spell you already cast today without spending its slot."},
|
|
58799
58929
|
],
|
|
58800
58930
|
help:[
|
|
58801
58931
|
["What does a wizard do?",
|
|
58802
58932
|
"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
58933
|
["Arcane school slot",
|
|
58804
|
-
"Your school (curriculum) gives you <b>one extra prepared slot of every rank</b> you can cast. Fill the
|
|
58934
|
+
"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
58935
|
["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
|
|
58936
|
+
"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
58937
|
],
|
|
58808
58938
|
},
|
|
58809
58939
|
|
|
58810
58940
|
/* ---------------- DRUID ---------------- */
|
|
58811
58941
|
druid:{
|
|
58812
|
-
id:"druid", name:"Druid", icon:"
|
|
58942
|
+
id:"druid", name:"Druid", icon:"druid", color:"#4faf6d",
|
|
58813
58943
|
tagline:"Primal prepared caster · nature's versatility",
|
|
58814
58944
|
tradition:"primal", keyAbility:"Wisdom", keyAbilityShort:"Wis",
|
|
58815
58945
|
casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
|
|
58816
|
-
preview:true,
|
|
58817
58946
|
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
58947
|
help:[
|
|
58819
58948
|
["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
|
|
58949
|
+
"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
58950
|
],
|
|
58822
58951
|
},
|
|
58823
58952
|
|
|
58824
58953
|
/* ---------------- WITCH ---------------- */
|
|
58825
58954
|
witch:{
|
|
58826
|
-
id:"witch", name:"Witch", icon:"
|
|
58955
|
+
id:"witch", name:"Witch", icon:"witch", color:"#b061b8",
|
|
58827
58956
|
tagline:"Prepared caster · your patron sets the tradition",
|
|
58828
58957
|
traditionFrom:"patron", defaultTradition:"occult",
|
|
58829
58958
|
keyAbility:"Intelligence", keyAbilityShort:"Int",
|
|
58830
58959
|
casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
|
|
58831
|
-
preview:true,
|
|
58832
58960
|
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
58961
|
help:[
|
|
58834
58962
|
["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>
|
|
58963
|
+
"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
58964
|
],
|
|
58837
58965
|
},
|
|
58838
58966
|
|
|
58839
58967
|
/* ---------------- SORCERER ---------------- */
|
|
58840
58968
|
sorcerer:{
|
|
58841
|
-
id:"sorcerer", name:"Sorcerer", icon:"
|
|
58969
|
+
id:"sorcerer", name:"Sorcerer", icon:"sorcerer", color:"#e05a52",
|
|
58842
58970
|
tagline:"Spontaneous caster · magic in the blood",
|
|
58843
58971
|
traditionFrom:"bloodline", keyAbility:"Charisma", keyAbilityShort:"Cha",
|
|
58844
58972
|
casting:"spontaneous", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
|
|
58845
|
-
preview:true,
|
|
58846
58973
|
features:[
|
|
58847
58974
|
{type:"signature"},
|
|
58848
58975
|
{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 +58984,17 @@ const CLASSES={
|
|
|
58857
58984
|
|
|
58858
58985
|
/* ---------------- BARD ---------------- */
|
|
58859
58986
|
bard:{
|
|
58860
|
-
id:"bard", name:"Bard", icon:"
|
|
58987
|
+
id:"bard", name:"Bard", icon:"bard", color:"#34a8c4",
|
|
58861
58988
|
tagline:"Occult spontaneous caster · master of support",
|
|
58862
58989
|
tradition:"occult", keyAbility:"Charisma", keyAbilityShort:"Cha",
|
|
58863
58990
|
casting:"spontaneous", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
|
|
58864
|
-
preview:true,
|
|
58865
58991
|
features:[
|
|
58866
58992
|
{type:"signature"},
|
|
58867
58993
|
{type:"note", title:"Compositions & muse", text:"Add your composition focus spells (Courageous Anthem, Counter Performance…) in the Focus section below."},
|
|
58868
58994
|
],
|
|
58869
58995
|
help:[
|
|
58870
58996
|
["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
|
|
58997
|
+
"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
58998
|
["Signature spells",
|
|
58873
58999
|
"Mark spells as <b>signature</b> (the ★) to heighten them with higher-rank slots. One per rank is the usual choice."],
|
|
58874
59000
|
],
|
|
@@ -58876,30 +59002,35 @@ const CLASSES={
|
|
|
58876
59002
|
|
|
58877
59003
|
/* ---------------- ORACLE ---------------- */
|
|
58878
59004
|
oracle:{
|
|
58879
|
-
id:"oracle", name:"Oracle", icon:"
|
|
59005
|
+
id:"oracle", name:"Oracle", icon:"oracle", color:"#c061d6",
|
|
58880
59006
|
tagline:"Divine spontaneous caster · power at a price",
|
|
58881
59007
|
tradition:"divine", keyAbility:"Charisma", keyAbilityShort:"Cha",
|
|
58882
59008
|
casting:"spontaneous", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
|
|
58883
|
-
preview:true,
|
|
58884
59009
|
features:[
|
|
58885
59010
|
{type:"signature"},
|
|
58886
|
-
{type:"
|
|
59011
|
+
{type:"stageTracker", key:"curse", icon:"oracle", label:"Oracular curse", max:4,
|
|
59012
|
+
note:"Your curse deepens by a stage each time you use a cursebound action — stronger effects, worse drawbacks. Resting clears it."},
|
|
59013
|
+
{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
59014
|
],
|
|
58888
59015
|
help:[
|
|
58889
59016
|
["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
|
|
59017
|
+
"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
59018
|
["Signature spells",
|
|
58892
59019
|
"Mark spells as <b>signature</b> (the ★) to heighten them with higher-rank slots — usually one per spell rank."],
|
|
58893
59020
|
],
|
|
58894
59021
|
},
|
|
58895
59022
|
/* ---------------- MAGUS ---------------- */
|
|
58896
59023
|
magus:{
|
|
58897
|
-
id:"magus", name:"Magus", icon:"
|
|
59024
|
+
id:"magus", name:"Magus", icon:"magus", color:"#5d82e0",
|
|
58898
59025
|
tagline:"Arcane half-caster · blade and spell as one",
|
|
58899
59026
|
tradition:"arcane", keyAbility:"Intelligence", keyAbilityShort:"Int",
|
|
58900
59027
|
casting:"prepared", slots:"partial", prof:PARTIAL_CASTER_PROF, cantrips:5,
|
|
58901
|
-
|
|
58902
|
-
|
|
59028
|
+
features:[
|
|
59029
|
+
{type:"studiousSlots", key:"studious", minLevel:7, rank:2, count:2, label:"Studious spells",
|
|
59030
|
+
spells:["gecko-grip","sure-strike","water-breathing"],
|
|
59031
|
+
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."},
|
|
59032
|
+
{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."},
|
|
59033
|
+
],
|
|
58903
59034
|
help:[
|
|
58904
59035
|
["What does a magus do?",
|
|
58905
59036
|
"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 +59041,13 @@ const CLASSES={
|
|
|
58910
59041
|
|
|
58911
59042
|
/* ---------------- SUMMONER ---------------- */
|
|
58912
59043
|
summoner:{
|
|
58913
|
-
id:"summoner", name:"Summoner", icon:"
|
|
59044
|
+
id:"summoner", name:"Summoner", icon:"summoner", color:"#2bb59a",
|
|
58914
59045
|
tagline:"Half-caster · you and your eidolon, as one",
|
|
58915
59046
|
traditionFrom:"patron", defaultTradition:"arcane",
|
|
58916
59047
|
traditionChoiceLabel:"Eidolon's tradition",
|
|
58917
59048
|
traditionChoiceHint:"your eidolon's essence sets your spell tradition",
|
|
58918
59049
|
keyAbility:"Charisma", keyAbilityShort:"Cha",
|
|
58919
59050
|
casting:"spontaneous", slots:"partial", prof:PARTIAL_CASTER_PROF, cantrips:5,
|
|
58920
|
-
preview:true,
|
|
58921
59051
|
features:[
|
|
58922
59052
|
{type:"signature"},
|
|
58923
59053
|
{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 +59062,15 @@ const CLASSES={
|
|
|
58932
59062
|
|
|
58933
59063
|
/* ---------------- PSYCHIC ---------------- */
|
|
58934
59064
|
psychic:{
|
|
58935
|
-
id:"psychic", name:"Psychic", icon:"
|
|
59065
|
+
id:"psychic", name:"Psychic", icon:"psychic", color:"#e05fa6",
|
|
58936
59066
|
tagline:"Occult spontaneous caster · few spells, mighty cantrips",
|
|
58937
59067
|
tradition:"occult", keyAbility:"Int or Cha", keyAbilityShort:"Key",
|
|
58938
|
-
casting:"spontaneous", slots:"psychic", prof:FULL_CASTER_PROF, cantrips:
|
|
58939
|
-
preview:true,
|
|
59068
|
+
casting:"spontaneous", slots:"psychic", prof:FULL_CASTER_PROF, cantrips:3,
|
|
58940
59069
|
features:[
|
|
58941
59070
|
{type:"signature"},
|
|
58942
|
-
{type:"
|
|
59071
|
+
{type:"dailyResource", key:"unleash", icon:"psychic", label:"Unleash Psyche", uses:1, verb:"Unleash", recharge:"refocus",
|
|
59072
|
+
note:"Surge your psyche for 2 rounds — bigger cantrips and better spell attacks. Afterward your mind is strained; Refocus to Unleash again."},
|
|
59073
|
+
{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
59074
|
],
|
|
58944
59075
|
help:[
|
|
58945
59076
|
["What does a psychic do?",
|
|
@@ -58951,15 +59082,14 @@ const CLASSES={
|
|
|
58951
59082
|
|
|
58952
59083
|
/* ---------------- ANIMIST ---------------- */
|
|
58953
59084
|
animist:{
|
|
58954
|
-
id:"animist", name:"Animist", icon:"
|
|
59085
|
+
id:"animist", name:"Animist", icon:"animist", color:"#e08a3c",
|
|
58955
59086
|
tagline:"Divine prepared caster · channels apparitions (spirits)",
|
|
58956
59087
|
tradition:"divine", keyAbility:"Wisdom", keyAbilityShort:"Wis",
|
|
58957
59088
|
casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:4,
|
|
58958
|
-
preview:true,
|
|
58959
59089
|
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
59090
|
help:[
|
|
58961
59091
|
["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
|
|
59092
|
+
"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
59093
|
["Cantrips & apparitions",
|
|
58964
59094
|
"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
59095
|
],
|
|
@@ -59050,14 +59180,16 @@ function loadLibrary(){
|
|
|
59050
59180
|
}catch(e){}
|
|
59051
59181
|
try{ // migrate a single v2 character
|
|
59052
59182
|
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}; }
|
|
59183
|
+
if(v2 && v2.classId){ const c=Object.assign(blankChar(), v2); return normalizeLib({characters:{[c.id]:c}, activeId:c.id}); }
|
|
59054
59184
|
}catch(e){}
|
|
59055
59185
|
const c=blankChar();
|
|
59056
|
-
return {characters:{[c.id]:c}, activeId:c.id};
|
|
59186
|
+
return normalizeLib({characters:{[c.id]:c}, activeId:c.id});
|
|
59057
59187
|
}
|
|
59188
|
+
function defaultSettings(){ return { themeMode:"auto", custom:null }; }
|
|
59058
59189
|
function normalizeLib(lib){
|
|
59059
59190
|
Object.keys(lib.characters).forEach(id=>{ lib.characters[id]=Object.assign(defaultStateFields(), lib.characters[id], {id}); });
|
|
59060
59191
|
if(!lib.characters[lib.activeId]) lib.activeId=Object.keys(lib.characters)[0];
|
|
59192
|
+
lib.settings=Object.assign(defaultSettings(), lib.settings||{});
|
|
59061
59193
|
return lib;
|
|
59062
59194
|
}
|
|
59063
59195
|
let _storageWarned=false;
|
|
@@ -59065,9 +59197,66 @@ function saveState(){
|
|
|
59065
59197
|
library.characters[state.id]=state;
|
|
59066
59198
|
try{ localStorage.setItem(LS_KEY, JSON.stringify(library)); }
|
|
59067
59199
|
catch(e){
|
|
59068
|
-
if(!_storageWarned && typeof toast==="function"){ toast("
|
|
59200
|
+
if(!_storageWarned && typeof toast==="function"){ toast("Couldn't save — storage full or disabled"); _storageWarned=true; }
|
|
59069
59201
|
}
|
|
59070
59202
|
}
|
|
59203
|
+
function saveSettings(){ try{ localStorage.setItem(LS_KEY, JSON.stringify(library)); }catch(e){} }
|
|
59204
|
+
|
|
59205
|
+
/* ============================================================
|
|
59206
|
+
THEME (light/dark · per-class accent · custom palette)
|
|
59207
|
+
The base palette flips with [data-theme]; --accent (and its readable
|
|
59208
|
+
ink) is set per active class, or overridden by a custom palette.
|
|
59209
|
+
============================================================ */
|
|
59210
|
+
const DEFAULT_ACCENT="#6c7a89";
|
|
59211
|
+
function hexToRgb(h){
|
|
59212
|
+
h=(h||"").replace("#","").trim();
|
|
59213
|
+
if(h.length===3) h=h.split("").map(c=>c+c).join("");
|
|
59214
|
+
const n=parseInt(h||"000000",16);
|
|
59215
|
+
return {r:(n>>16)&255, g:(n>>8)&255, b:n&255};
|
|
59216
|
+
}
|
|
59217
|
+
function relLuminance(hex){
|
|
59218
|
+
const {r,g,b}=hexToRgb(hex);
|
|
59219
|
+
const f=v=>{ v/=255; return v<=.03928 ? v/12.92 : Math.pow((v+.055)/1.055,2.4); };
|
|
59220
|
+
return .2126*f(r)+.7152*f(g)+.0722*f(b);
|
|
59221
|
+
}
|
|
59222
|
+
function inkFor(hex){ return relLuminance(hex) < .48 ? "#ffffff" : "#15171c"; }
|
|
59223
|
+
function mixHex(a,b,t){
|
|
59224
|
+
const A=hexToRgb(a), B=hexToRgb(b);
|
|
59225
|
+
const c=k=>Math.round(A[k]+(B[k]-A[k])*t).toString(16).padStart(2,"0");
|
|
59226
|
+
return "#"+c("r")+c("g")+c("b");
|
|
59227
|
+
}
|
|
59228
|
+
function resolveThemeMode(){
|
|
59229
|
+
const m=(library.settings&&library.settings.themeMode)||"auto";
|
|
59230
|
+
if(m==="light"||m==="dark") return m;
|
|
59231
|
+
try{ return (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light"; }
|
|
59232
|
+
catch(e){ return "dark"; }
|
|
59233
|
+
}
|
|
59234
|
+
function applyTheme(){
|
|
59235
|
+
const root=document.documentElement;
|
|
59236
|
+
const custom=(library.settings&&library.settings.custom)||null;
|
|
59237
|
+
root.dataset.theme=resolveThemeMode();
|
|
59238
|
+
const accent=(custom&&custom.accent) || (activeClass()&&activeClass().color) || DEFAULT_ACCENT;
|
|
59239
|
+
root.style.setProperty("--accent", accent);
|
|
59240
|
+
root.style.setProperty("--accent-ink", inkFor(accent));
|
|
59241
|
+
// optional full-palette overrides (background / text)
|
|
59242
|
+
if(custom&&custom.bg) root.style.setProperty("--bg", custom.bg); else root.style.removeProperty("--bg");
|
|
59243
|
+
if(custom&&custom.ink) root.style.setProperty("--ink", custom.ink); else root.style.removeProperty("--ink");
|
|
59244
|
+
if(custom&&custom.surface){
|
|
59245
|
+
root.style.setProperty("--surface", custom.surface);
|
|
59246
|
+
root.style.setProperty("--surface-2", mixHex(custom.surface, custom.ink||"#808080", .10));
|
|
59247
|
+
}else{ root.style.removeProperty("--surface"); root.style.removeProperty("--surface-2"); }
|
|
59248
|
+
// keep the browser chrome colour in step with the background
|
|
59249
|
+
const meta=document.querySelector('meta[name="theme-color"]');
|
|
59250
|
+
if(meta){ const bg=getComputedStyle(root).getPropertyValue("--bg").trim(); if(bg) meta.setAttribute("content", bg); }
|
|
59251
|
+
}
|
|
59252
|
+
function themeColorValue(token){ return (getComputedStyle(document.documentElement).getPropertyValue(token)||"").trim()||"#000000"; }
|
|
59253
|
+
function setThemeMode(m){ library.settings.themeMode=m; saveSettings(); applyTheme(); renderMenu(); }
|
|
59254
|
+
function setCustomColor(key,val){
|
|
59255
|
+
library.settings.custom=library.settings.custom||{};
|
|
59256
|
+
library.settings.custom[key]=val;
|
|
59257
|
+
saveSettings(); applyTheme();
|
|
59258
|
+
}
|
|
59259
|
+
function resetTheme(){ library.settings.custom=null; saveSettings(); applyTheme(); renderMenu(); }
|
|
59071
59260
|
|
|
59072
59261
|
/* ============================================================
|
|
59073
59262
|
CLASS-AWARE HELPERS
|
|
@@ -59130,7 +59319,7 @@ function actionLabel(a){
|
|
|
59130
59319
|
}
|
|
59131
59320
|
const m=String(a).match(/^([1-3])\s*(?:to|or|–|-)\s*([1-3])$/);
|
|
59132
59321
|
if(m) return `${ACTION_GLYPH[m[1]]}–${ACTION_GLYPH[m[2]]} ${m[1]} to ${m[2]} actions`;
|
|
59133
|
-
return
|
|
59322
|
+
return a;
|
|
59134
59323
|
}
|
|
59135
59324
|
function titleCaseTrait(t){ return t.charAt(0).toUpperCase()+t.slice(1); }
|
|
59136
59325
|
function escapeHtml(s){ return (s||"").replace(/[&<>]/g,c=>({"&":"&","<":"<",">":">"}[c])); }
|
|
@@ -59158,12 +59347,11 @@ function scaleDamage(s, castRank){
|
|
|
59158
59347
|
}
|
|
59159
59348
|
function damageChipHTML(s, castRank){
|
|
59160
59349
|
const ds=scaleDamage(s, castRank); if(!ds) return "";
|
|
59161
|
-
|
|
59162
|
-
const
|
|
59350
|
+
return ds.map(d=>{
|
|
59351
|
+
const cls=d.healing?"heal":"dmg";
|
|
59163
59352
|
const ty=(d.type&&!d.healing)?" "+escapeHtml(d.type):"";
|
|
59164
|
-
return
|
|
59165
|
-
});
|
|
59166
|
-
return `<span class="dmgchip">${parts.join(" ")}</span>`;
|
|
59353
|
+
return `<span class="dmgchip ${cls}">${escapeHtml(d.formula)}${ty}</span>`;
|
|
59354
|
+
}).join(" ");
|
|
59167
59355
|
}
|
|
59168
59356
|
|
|
59169
59357
|
function spellCardHTML(s, opts){
|
|
@@ -59176,7 +59364,7 @@ function spellCardHTML(s, opts){
|
|
|
59176
59364
|
if(s.duration) m.push(`<b>Duration</b> ${escapeHtml(s.duration)}`);
|
|
59177
59365
|
const meta = m.length?`<div class="meta">${m.join(" · ")}</div>`:"";
|
|
59178
59366
|
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"
|
|
59367
|
+
const saveTag = s.save ? `<span class="save-tag">${escapeHtml(s.save)}</span>` : "";
|
|
59180
59368
|
const heighten = s.heightened ? `<div class="heighten"><b>Heightened</b> ${textToHtml(s.heightened)}</div>` : "";
|
|
59181
59369
|
const note=legacyNote(s)?`<div class="meta" style="margin:-2px 0 4px"><span class="formerly">${escapeHtml(legacyNote(s))}</span></div>`:"";
|
|
59182
59370
|
return `
|
|
@@ -59199,7 +59387,7 @@ function spellCardHTML(s, opts){
|
|
|
59199
59387
|
function spellPreviewHTML(s, castRank){
|
|
59200
59388
|
if(!s) return "";
|
|
59201
59389
|
const heightNote=(castRank>s.rank)?` · cast at rank ${castRank}`:"";
|
|
59202
|
-
const saveTag=s.save?` ·
|
|
59390
|
+
const saveTag=s.save?` · ${escapeHtml(s.save)}`:"";
|
|
59203
59391
|
const m=[];
|
|
59204
59392
|
if(s.range) m.push(`<b>Range</b> ${escapeHtml(s.range)}`);
|
|
59205
59393
|
if(s.area) m.push(`<b>Area</b> ${escapeHtml(s.area)}`);
|
|
@@ -59251,7 +59439,7 @@ function toast(msg){
|
|
|
59251
59439
|
function charSummary(c){
|
|
59252
59440
|
if(!c.classId) return "New character — no class yet";
|
|
59253
59441
|
const cl=CLASSES[c.classId];
|
|
59254
|
-
return `${cl.
|
|
59442
|
+
return `${cl.name} · Level ${c.level}`;
|
|
59255
59443
|
}
|
|
59256
59444
|
function openMenu(){
|
|
59257
59445
|
VIEWS.forEach(v=>document.getElementById("view-"+v).classList.add("hide"));
|
|
@@ -59268,18 +59456,33 @@ function renderMenu(){
|
|
|
59268
59456
|
const c=library.characters[id];
|
|
59269
59457
|
const active=id===library.activeId;
|
|
59270
59458
|
return `<div class="classcard ${active?"activechar":""}">
|
|
59271
|
-
<div class="ic">${c.classId?CLASSES[c.classId].icon:"
|
|
59459
|
+
<div class="ic" style="color:${c.classId?CLASSES[c.classId].color:"var(--muted)"}">${c.classId?iconSvg(CLASSES[c.classId].icon):iconSvg("plus")}</div>
|
|
59272
59460
|
<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(--
|
|
59461
|
+
<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
59462
|
<div class="tl">${charSummary(c)}</div>
|
|
59275
59463
|
</div>
|
|
59276
59464
|
${ids.length>1?`<button class="rmrow" title="delete" onclick="deleteCharacter('${id}')">✕</button>`:""}
|
|
59277
59465
|
</div>`;
|
|
59278
59466
|
}).join("");
|
|
59279
59467
|
document.getElementById("menuList").innerHTML=list;
|
|
59468
|
+
renderAppearance();
|
|
59280
59469
|
showInstallButton(!!deferredInstall);
|
|
59281
59470
|
const ds=document.getElementById("dataStamp"); if(ds) ds.textContent=dataStampText();
|
|
59282
59471
|
}
|
|
59472
|
+
/* Appearance panel: Light/Dark/Auto + a four-swatch custom palette. */
|
|
59473
|
+
function renderAppearance(){
|
|
59474
|
+
const box=document.getElementById("appearancePanel"); if(!box) return;
|
|
59475
|
+
const mode=(library.settings&&library.settings.themeMode)||"auto";
|
|
59476
|
+
const seg=["light","dark","auto"].map(m=>
|
|
59477
|
+
`<button class="${mode===m?"on":""}" onclick="setThemeMode('${m}')">${m[0].toUpperCase()+m.slice(1)}</button>`).join("");
|
|
59478
|
+
const rows=[["Background","bg","--bg"],["Surface","surface","--surface"],["Text","ink","--ink"],["Accent","accent","--accent"]]
|
|
59479
|
+
.map(([label,key,token])=>`<div class="swatchrow"><label>${label}</label>
|
|
59480
|
+
<input type="color" value="${themeColorValue(token)}" oninput="setCustomColor('${key}',this.value)" aria-label="${label} colour"></div>`).join("");
|
|
59481
|
+
box.innerHTML=`<div class="seg" id="themeSeg">${seg}</div>
|
|
59482
|
+
<p class="meta">Colours follow each class's theme by default. Pick your own below — they apply to every character.</p>
|
|
59483
|
+
${rows}
|
|
59484
|
+
<button class="btn secondary" onclick="resetTheme()">Reset to class default</button>`;
|
|
59485
|
+
}
|
|
59283
59486
|
function newCharacter(){ const c=blankChar(); library.characters[c.id]=c; library.activeId=c.id; state=c; saveState(); showClassPicker(); }
|
|
59284
59487
|
function switchCharacter(id){
|
|
59285
59488
|
if(!library.characters[id]) return;
|
|
@@ -59305,7 +59508,7 @@ function exportCharacter(){
|
|
|
59305
59508
|
const code="PF2E1:"+b64encode(JSON.stringify(c));
|
|
59306
59509
|
const io=document.getElementById("menuIO");
|
|
59307
59510
|
io.value=code; io.focus(); io.select();
|
|
59308
|
-
if(navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(code).then(()=>toast("
|
|
59511
|
+
if(navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(code).then(()=>toast("Copied to clipboard")).catch(()=>toast("Code ready — copy it")); }
|
|
59309
59512
|
else toast("Code ready — copy it");
|
|
59310
59513
|
}
|
|
59311
59514
|
function importCharacter(){
|
|
@@ -59317,7 +59520,7 @@ function importCharacter(){
|
|
|
59317
59520
|
if(typeof obj!=="object"||!("level" in obj)) throw new Error("bad");
|
|
59318
59521
|
const c=Object.assign(blankChar(), obj, {id:uid()});
|
|
59319
59522
|
library.characters[c.id]=c; library.activeId=c.id; state=c; saveState();
|
|
59320
|
-
toast("
|
|
59523
|
+
toast("Character imported");
|
|
59321
59524
|
if(!state.classId){ showClassPicker(); } else { renderHeader(); go(state.prepared?"today":"prepare"); }
|
|
59322
59525
|
}catch(e){ alert("That code didn't work — make sure you pasted the whole thing."); }
|
|
59323
59526
|
}
|
|
@@ -59332,18 +59535,19 @@ function showClassPicker(){
|
|
|
59332
59535
|
const sec=document.getElementById("view-classpick");
|
|
59333
59536
|
sec.classList.remove("hide");
|
|
59334
59537
|
sec.innerHTML=`
|
|
59335
|
-
<h1 style="text-align:center"
|
|
59538
|
+
<h1 style="text-align:center">PF2e Spellbook</h1>
|
|
59336
59539
|
<p class="meta" style="text-align:center;margin-bottom:18px">Choose your class to begin.</p>
|
|
59337
59540
|
${CLASS_ORDER.map(id=>{
|
|
59338
59541
|
const c=CLASSES[id];
|
|
59339
|
-
return `<button class="classcard" onclick="chooseClass('${id}')">
|
|
59340
|
-
<div class="ic">${c.icon}</div>
|
|
59542
|
+
return `<button class="classcard" style="border-left-color:${c.color}" onclick="chooseClass('${id}')">
|
|
59543
|
+
<div class="ic" style="color:${c.color}">${iconSvg(c.icon)}</div>
|
|
59341
59544
|
<div class="txt"><div class="nm">${c.name}${c.preview?` <span class="previewtag">preview</span>`:""}</div>
|
|
59342
59545
|
<div class="tl">${c.tagline}</div></div>
|
|
59343
59546
|
<div class="arr">→</div>
|
|
59344
59547
|
</button>`;
|
|
59345
59548
|
}).join("")}
|
|
59346
59549
|
<p class="meta" style="text-align:center;margin-top:18px;font-size:.8rem">Pathfinder 2e (Remaster) · unofficial fan tool<br>${escapeHtml(dataStampText())}</p>`;
|
|
59550
|
+
applyTheme();
|
|
59347
59551
|
window.scrollTo(0,0);
|
|
59348
59552
|
}
|
|
59349
59553
|
function chooseClass(id){
|
|
@@ -59354,8 +59558,9 @@ function chooseClass(id){
|
|
|
59354
59558
|
if(!hasFeature("divineFont")) state.font="heal";
|
|
59355
59559
|
if(c.traditionFrom==="patron" && !state.patronTradition) state.patronTradition=c.defaultTradition||"occult";
|
|
59356
59560
|
saveState();
|
|
59561
|
+
applyTheme();
|
|
59357
59562
|
renderHeader();
|
|
59358
|
-
toast(`${c.
|
|
59563
|
+
toast(`${c.name} selected`);
|
|
59359
59564
|
go(state.prepared ? "today" : "prepare");
|
|
59360
59565
|
}
|
|
59361
59566
|
function openClassPicker(){ showClassPicker(); }
|
|
@@ -59365,7 +59570,8 @@ function openClassPicker(){ showClassPicker(); }
|
|
|
59365
59570
|
============================================================ */
|
|
59366
59571
|
function renderHeader(){
|
|
59367
59572
|
const c=activeClass(); if(!c) return;
|
|
59368
|
-
|
|
59573
|
+
applyTheme();
|
|
59574
|
+
document.getElementById("charIcon").innerHTML=iconSvg(c.icon);
|
|
59369
59575
|
document.getElementById("charNameHead").textContent=state.name||c.name;
|
|
59370
59576
|
document.getElementById("charSub").textContent=
|
|
59371
59577
|
`${c.name} · Pathfinder 2e (Remaster) · ${TRADITION_LABEL[activeTradition()]} list`;
|
|
@@ -59431,8 +59637,8 @@ function renderCharExtra(){
|
|
|
59431
59637
|
<label class="field"><span class="name">Divine Font</span>
|
|
59432
59638
|
<span class="hint">Which one did you choose at level 1?</span></label>
|
|
59433
59639
|
<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')"
|
|
59640
|
+
<button data-font="heal" class="heal ${state.font==="heal"?"on":""}" onclick="setFont('heal')">${iconSvg("heal")} Healing Font</button>
|
|
59641
|
+
<button data-font="harm" class="harm ${state.font==="harm"?"on":""}" onclick="setFont('harm')">${iconSvg("harm")} Harmful Font</button>
|
|
59436
59642
|
</div>`;
|
|
59437
59643
|
}
|
|
59438
59644
|
if(c.traditionFrom==="bloodline"){
|
|
@@ -59463,22 +59669,25 @@ function renderPrepSummary(){
|
|
|
59463
59669
|
let extra="";
|
|
59464
59670
|
if(hasFeature("divineFont")){
|
|
59465
59671
|
const fontName=state.font==="heal"?"Heal":"Harm";
|
|
59466
|
-
extra+=`<div style="margin-top:4px">${state.font==="heal"?"
|
|
59672
|
+
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
59673
|
}
|
|
59468
59674
|
getFeatures("extraSlots").forEach(f=>{
|
|
59469
|
-
extra+=`<div style="margin-top:4px">${f.icon} ${f.label}: <b>+1</b> per rank</div>`;
|
|
59675
|
+
extra+=`<div style="margin-top:4px">${iconSvg(f.icon)} ${f.label}: <b>+1</b> per rank</div>`;
|
|
59470
59676
|
});
|
|
59471
59677
|
getFeatures("dailyResource").forEach(f=>{
|
|
59472
|
-
extra+=`<div style="margin-top:4px">${f.icon} ${f.label}: <b>${f.uses}×</b>/day</div>`;
|
|
59678
|
+
extra+=`<div style="margin-top:4px">${iconSvg(f.icon)} ${f.label}: <b>${f.uses}×</b>/day</div>`;
|
|
59679
|
+
});
|
|
59680
|
+
studiousFeats().forEach(f=>{
|
|
59681
|
+
extra+=`<div style="margin-top:4px">${iconSvg("school")} ${f.label}: <b>${f.count}×</b> rank ${f.rank} (utility)</div>`;
|
|
59473
59682
|
});
|
|
59474
59683
|
if(hasFocus()){
|
|
59475
|
-
extra+=`<div style="margin-top:4px"
|
|
59684
|
+
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
59685
|
}
|
|
59477
59686
|
document.getElementById("prepSummary").innerHTML=`
|
|
59478
59687
|
<div class="meta" style="font-size:1rem">
|
|
59479
|
-
<div
|
|
59480
|
-
<div style="margin-top:8px"
|
|
59481
|
-
<div style="margin-top:4px"
|
|
59688
|
+
<div><b>Spell DC ${spellDC()}</b> · Spell attack ${spellAtk()}</div>
|
|
59689
|
+
<div style="margin-top:8px">Cantrips ${isSpontaneous()?"known":""}: <b>${c.cantrips}</b></div>
|
|
59690
|
+
<div style="margin-top:4px">${isSpontaneous()?"Slots (cast any known spell of that rank)":"Spell slots"}: ${slotText||"—"}</div>
|
|
59482
59691
|
${extra}
|
|
59483
59692
|
</div>`;
|
|
59484
59693
|
}
|
|
@@ -59504,6 +59713,7 @@ function renderPrepDynamic(){
|
|
|
59504
59713
|
getFeatures("note").forEach(f=>{ html+=`<div class="banner"><b>${f.title}</b> — ${f.text}</div>`; });
|
|
59505
59714
|
html+=cantripPicksHTML();
|
|
59506
59715
|
html+= isSpontaneous() ? repertoireHTML() : preparedSlotsHTML();
|
|
59716
|
+
if(!isSpontaneous()) html+=studiousSlotsHTML();
|
|
59507
59717
|
if(hasFocus()) html+=focusPicksHTML();
|
|
59508
59718
|
document.getElementById("prepDynamic").innerHTML=html;
|
|
59509
59719
|
}
|
|
@@ -59522,14 +59732,14 @@ function focusPicksHTML(){
|
|
|
59522
59732
|
<span class="fcname">${escapeHtml(s.name)}</span><span class="fcrank">${rank}</span></label>`;
|
|
59523
59733
|
}).join("");
|
|
59524
59734
|
return `<div class="slotgroup">
|
|
59525
|
-
<h3
|
|
59735
|
+
<h3>Focus spells <span class="count" style="font-weight:600;color:var(--muted)">(pool of ${pool})</span></h3>
|
|
59526
59736
|
<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
59737
|
<label class="field" style="margin:8px 0"><span class="name">Focus Point pool</span>
|
|
59528
59738
|
<span class="hint">1 + 1 per extra focus feat, up to 3</span>
|
|
59529
59739
|
<select id="inFocusPool" onchange="setFocusPool(this.value)">
|
|
59530
59740
|
${[1,2,3].map(n=>`<option value="${n}" ${n===pool?"selected":""}>${n} Focus Point${n>1?"s":""}</option>`).join("")}
|
|
59531
59741
|
</select></label>
|
|
59532
|
-
<input type="text" id="focusFilter" placeholder="
|
|
59742
|
+
<input type="text" id="focusFilter" placeholder="Filter focus spells (name or trait)…" oninput="filterFocusChecklist(this.value)" aria-label="filter focus spells">
|
|
59533
59743
|
<div class="focus-checklist" id="focusChecklist">${rows}</div>
|
|
59534
59744
|
</div>`;
|
|
59535
59745
|
}
|
|
@@ -59547,7 +59757,7 @@ function gatherFocus(){
|
|
|
59547
59757
|
function cantripPicksHTML(){
|
|
59548
59758
|
const c=activeClass();
|
|
59549
59759
|
const prev=(state.prepared&&state.prepared.cantrips)||[];
|
|
59550
|
-
let html=`<div class="slotgroup"><h3
|
|
59760
|
+
let html=`<div class="slotgroup"><h3>Cantrips (pick ${c.cantrips})</h3>`;
|
|
59551
59761
|
for(let i=0;i<c.cantrips;i++){
|
|
59552
59762
|
const sel=prev[i]||"", pid="cprev"+i;
|
|
59553
59763
|
html+=`<div class="prep-slot">
|
|
@@ -59570,10 +59780,10 @@ function preparedSlotsHTML(){
|
|
|
59570
59780
|
const fontName=state.font==="heal"?"Heal":"Harm";
|
|
59571
59781
|
const fontSpell=findSpell(state.font==="heal"?feat.healSlug:feat.harmSlug);
|
|
59572
59782
|
html+=`<div class="slotgroup">
|
|
59573
|
-
<h3>${state.font==="heal"?"
|
|
59783
|
+
<h3>${iconSvg(state.font==="heal"?"heal":"harm")} Divine Font — ${fontName} (free slots)</h3>
|
|
59574
59784
|
<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
59785
|
for(let i=0;i<fontCount();i++){
|
|
59576
|
-
html+=`<div class="slotrow"><div class="num">${state.font==="heal"?"
|
|
59786
|
+
html+=`<div class="slotrow"><div class="num">${iconSvg(state.font==="heal"?"heal":"harm")}</div>
|
|
59577
59787
|
<select disabled><option>${fontName} (rank ${hr})</option></select></div>`;
|
|
59578
59788
|
}
|
|
59579
59789
|
html+=`<div class="prep-preview">${spellPreviewHTML(fontSpell, hr)}</div></div>`;
|
|
@@ -59583,7 +59793,7 @@ function preparedSlotsHTML(){
|
|
|
59583
59793
|
Object.keys(slots).map(Number).sort((a,b)=>b-a).forEach(r=>{
|
|
59584
59794
|
const n=slots[r];
|
|
59585
59795
|
const prev=prevSlots[r]||[];
|
|
59586
|
-
html+=`<div class="slotgroup"><h3
|
|
59796
|
+
html+=`<div class="slotgroup"><h3>Rank ${r} slots (${n})</h3>`;
|
|
59587
59797
|
for(let i=0;i<n;i++){
|
|
59588
59798
|
const sel=prev[i]||"", pid="sprev"+r+"_"+i;
|
|
59589
59799
|
html+=`<div class="prep-slot">
|
|
@@ -59596,7 +59806,7 @@ function preparedSlotsHTML(){
|
|
|
59596
59806
|
const ex=(prevExtra[f.key]&&prevExtra[f.key][r]&&prevExtra[f.key][r][0])||"";
|
|
59597
59807
|
const pid="xprev"+f.key+r;
|
|
59598
59808
|
html+=`<div class="prep-slot">
|
|
59599
|
-
<div class="slotrow"><div class="num">${f.icon}</div>
|
|
59809
|
+
<div class="slotrow"><div class="num">${iconSvg(f.icon)}</div>
|
|
59600
59810
|
<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
59811
|
<div class="prep-preview" id="${pid}">${spellPreviewHTML(findSpell(ex),r)}</div></div>`;
|
|
59602
59812
|
});
|
|
@@ -59605,6 +59815,33 @@ function preparedSlotsHTML(){
|
|
|
59605
59815
|
return html;
|
|
59606
59816
|
}
|
|
59607
59817
|
|
|
59818
|
+
/* Studious spells (magus, level 7+): a couple of extra fixed-rank slots that can
|
|
59819
|
+
only hold a short list of utility spells. Restricted <select> so the app stays
|
|
59820
|
+
honest about what's legal in these slots. */
|
|
59821
|
+
function studiousFeats(){ return getFeatures("studiousSlots").filter(f=>Number(state.level)>=(f.minLevel||1)); }
|
|
59822
|
+
function studiousSlotsHTML(){
|
|
59823
|
+
const feats=studiousFeats(); if(!feats.length) return "";
|
|
59824
|
+
const prevAll=(state.prepared&&state.prepared.studious)||{};
|
|
59825
|
+
let html="";
|
|
59826
|
+
feats.forEach(f=>{
|
|
59827
|
+
const prev=prevAll[f.key]||[];
|
|
59828
|
+
const opts=(f.spells||[]).map(findSpell).filter(Boolean).sort((a,b)=>a.name.localeCompare(b.name));
|
|
59829
|
+
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>
|
|
59830
|
+
<p class="meta">${escapeHtml(f.note||"")}</p>`;
|
|
59831
|
+
for(let i=0;i<f.count;i++){
|
|
59832
|
+
const sel=prev[i]||"", pid="stud_"+f.key+"_"+i;
|
|
59833
|
+
const optionHtml=`<option value="">— choose —</option>`+
|
|
59834
|
+
opts.map(s=>`<option value="${s.slug}" ${s.slug===sel?"selected":""}>${escapeHtml(s.name)}</option>`).join("");
|
|
59835
|
+
html+=`<div class="prep-slot">
|
|
59836
|
+
<div class="slotrow"><div class="num">${iconSvg("school")}</div>
|
|
59837
|
+
<select class="studiousSel" data-key="${f.key}" data-castrank="${f.rank}" data-preview="${pid}" onchange="updatePreview(this)">${optionHtml}</select></div>
|
|
59838
|
+
<div class="prep-preview" id="${pid}">${spellPreviewHTML(findSpell(sel),f.rank)}</div></div>`;
|
|
59839
|
+
}
|
|
59840
|
+
html+=`</div>`;
|
|
59841
|
+
});
|
|
59842
|
+
return html;
|
|
59843
|
+
}
|
|
59844
|
+
|
|
59608
59845
|
let repUID=0;
|
|
59609
59846
|
function repRowHTML(rank, sel, sig){
|
|
59610
59847
|
const pid="rprev"+(repUID++);
|
|
@@ -59628,7 +59865,7 @@ function repertoireHTML(){
|
|
|
59628
59865
|
const count=Math.max(known.length, slots[r]||0);
|
|
59629
59866
|
if(!count) continue; // no slots of this rank (e.g. a summoner's shed low ranks) → no repertoire of that rank
|
|
59630
59867
|
html+=`<div class="slotgroup" data-reprank="${r}">
|
|
59631
|
-
<h3
|
|
59868
|
+
<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
59869
|
<div class="repRows" id="repRows-${r}">`;
|
|
59633
59870
|
for(let i=0;i<count;i++){
|
|
59634
59871
|
const e=known[i]||{};
|
|
@@ -59668,8 +59905,11 @@ function savePrep(){
|
|
|
59668
59905
|
document.querySelectorAll(".slotSel").forEach(sel=>{ const r=sel.dataset.rank; (slots[r]=slots[r]||[]).push(sel.value); });
|
|
59669
59906
|
const extra={};
|
|
59670
59907
|
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); });
|
|
59908
|
+
const studious={};
|
|
59909
|
+
document.querySelectorAll(".studiousSel").forEach(sel=>{ const k=sel.dataset.key; (studious[k]=studious[k]||[]).push(sel.value); });
|
|
59671
59910
|
const hr=highestRank(state.level);
|
|
59672
59911
|
state.prepared={ type:"prepared", cantrips, slots, extra };
|
|
59912
|
+
if(Object.keys(studious).length) state.prepared.studious=studious;
|
|
59673
59913
|
if(hasFeature("divineFont")){
|
|
59674
59914
|
const feat=getFeature("divineFont");
|
|
59675
59915
|
state.prepared.divineFont={ font:state.font, fontName:state.font==="heal"?"Heal":"Harm",
|
|
@@ -59678,7 +59918,7 @@ function savePrep(){
|
|
|
59678
59918
|
}
|
|
59679
59919
|
if(focus) state.prepared.focus=focus;
|
|
59680
59920
|
saveState();
|
|
59681
|
-
toast("
|
|
59921
|
+
toast("Prepared for the day!");
|
|
59682
59922
|
go("today");
|
|
59683
59923
|
}
|
|
59684
59924
|
|
|
@@ -59691,7 +59931,7 @@ function todayHeaderHTML(){
|
|
|
59691
59931
|
return `<div class="card" style="text-align:center">
|
|
59692
59932
|
<div style="font-size:1.1rem"><b>${escapeHtml(state.name||c.name)}</b> · ${c.name} · Level ${state.level}</div>
|
|
59693
59933
|
<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()"
|
|
59934
|
+
<button class="btn secondary" style="margin-top:12px" onclick="newDay()">Rest & reset the day</button>
|
|
59695
59935
|
</div>`;
|
|
59696
59936
|
}
|
|
59697
59937
|
function dailyResourceHTML(){
|
|
@@ -59701,14 +59941,34 @@ function dailyResourceHTML(){
|
|
|
59701
59941
|
const spent=state.cast[key]||0, remaining=f.uses-spent, done=remaining<=0;
|
|
59702
59942
|
let pips=""; for(let k=0;k<f.uses;k++){ pips+=`<span class="pip ${k<remaining?"full":"spent"}"></span>`; }
|
|
59703
59943
|
const btn=done?`<button class="castbtn zero" onclick="uncast('${key}')">↩ undo</button>`
|
|
59704
|
-
:`<button class="castbtn" onclick="doCast('${key}',${f.uses})"
|
|
59944
|
+
:`<button class="castbtn" onclick="doCast('${key}',${f.uses})">${f.verb||"Use"}</button>`;
|
|
59705
59945
|
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>
|
|
59946
|
+
<div class="cast-top"><div class="nm">${iconSvg(f.icon)} ${f.label}</div><div class="uses">${pips} ${btn}</div></div>
|
|
59707
59947
|
<div class="meta">${escapeHtml(f.note||"")}</div></div>`;
|
|
59708
59948
|
});
|
|
59709
59949
|
return html;
|
|
59710
59950
|
}
|
|
59711
59951
|
|
|
59952
|
+
/* Escalating-stage tracker (e.g. the oracle's cursebound curse). A simple
|
|
59953
|
+
0..max stepper that persists in state.cast and resets on a new day. */
|
|
59954
|
+
function stageTrackerHTML(){
|
|
59955
|
+
let html="";
|
|
59956
|
+
getFeatures("stageTracker").forEach(f=>{
|
|
59957
|
+
const k="stage:"+f.key;
|
|
59958
|
+
const cur=Math.max(0, Math.min(f.max, state.cast[k]||0));
|
|
59959
|
+
let pips=""; for(let i=1;i<=f.max;i++){ pips+=`<span class="pip ${i<=cur?"full":"spent"}"></span>`; }
|
|
59960
|
+
html+=`<div class="cast-card">
|
|
59961
|
+
<div class="cast-top"><div class="nm">${iconSvg(f.icon)} ${f.label} <span class="meta">stage ${cur} / ${f.max}</span></div>
|
|
59962
|
+
<div class="uses">${pips}
|
|
59963
|
+
<button class="castbtn zero" onclick="stageDown('${f.key}')" ${cur<=0?"disabled":""} aria-label="lower ${escapeHtml(f.label)} stage">−</button>
|
|
59964
|
+
<button class="castbtn" onclick="stageUp('${f.key}')" ${cur>=f.max?"disabled":""} aria-label="raise ${escapeHtml(f.label)} stage">+</button></div></div>
|
|
59965
|
+
<div class="meta">${escapeHtml(f.note||"")}</div></div>`;
|
|
59966
|
+
});
|
|
59967
|
+
return html;
|
|
59968
|
+
}
|
|
59969
|
+
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(); }
|
|
59970
|
+
function stageDown(key){ const k="stage:"+key; state.cast[k]=Math.max(0,(state.cast[k]||0)-1); saveState(); renderToday(); }
|
|
59971
|
+
|
|
59712
59972
|
function renderToday(){
|
|
59713
59973
|
renderHeader();
|
|
59714
59974
|
const p=state.prepared;
|
|
@@ -59729,41 +59989,58 @@ function focusSectionHTML(p){
|
|
|
59729
59989
|
const fr=focusRank();
|
|
59730
59990
|
let pips=""; for(let k=0;k<pool;k++){ pips+=`<span class="pip ${k<remaining?"full":"spent"}"></span>`; }
|
|
59731
59991
|
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"
|
|
59992
|
+
let html=`<div class="sectionhead">Focus spells <span class="count">${remaining}/${pool} point${pool>1?"s":""}</span></div>
|
|
59733
59993
|
<div style="margin:2px 0 8px"><span class="uses">${pips}${refocus}</span></div><div class="divider"></div>`;
|
|
59994
|
+
const psychic = !!(activeClass() && activeClass().id==="psychic");
|
|
59734
59995
|
f.spells.forEach((slug,i)=>{
|
|
59735
59996
|
const s=findSpell(slug); if(!s) return;
|
|
59736
59997
|
const atWill = s.rank===0;
|
|
59737
59998
|
const out = !atWill && remaining<=0;
|
|
59738
59999
|
const castRank=fr;
|
|
59739
60000
|
const heightNote=(castRank>s.rank)?`<span class="meta"> · ↑ heightened to rank ${castRank}</span>`:"";
|
|
59740
|
-
|
|
59741
|
-
|
|
59742
|
-
|
|
60001
|
+
// Psychic psi cantrips are free at will, but can be Amped by spending a Focus Point.
|
|
60002
|
+
let btn;
|
|
60003
|
+
if(atWill && psychic){
|
|
60004
|
+
const amp = remaining>0
|
|
60005
|
+
? `<button class="castbtn" onclick="castFocus(${pool})" title="Amp — spend 1 Focus Point">Amp (1 pt)</button>`
|
|
60006
|
+
: `<button class="castbtn zero" disabled>No points</button>`;
|
|
60007
|
+
btn = `<span class="unlim">∞ at will</span> ${amp}`;
|
|
60008
|
+
} else if(atWill){
|
|
60009
|
+
btn = `<span class="unlim">∞ at will</span>`;
|
|
60010
|
+
} else {
|
|
60011
|
+
btn = out ? `<button class="castbtn zero" disabled>No points</button>`
|
|
60012
|
+
: `<button class="castbtn" onclick="castFocus(${pool})">Cast</button>`;
|
|
60013
|
+
}
|
|
59743
60014
|
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?` ·
|
|
60015
|
+
html+=`<div class="cast-card ${out?"spent":""}" style="border-left-color:var(--accent)">
|
|
60016
|
+
<div class="cast-top"><div class="nm">${escapeHtml(s.name)} ${heightNote}</div><div class="uses">${btn}</div></div>
|
|
60017
|
+
<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
60018
|
<button class="detailsbtn" onclick="toggleDetails('${detailsId}',this)">Show details ▾</button>
|
|
59748
60019
|
<div id="${detailsId}" class="hide" style="margin-top:10px">${spellCardHTML(s)}</div></div>`;
|
|
59749
60020
|
});
|
|
59750
60021
|
return html;
|
|
59751
60022
|
}
|
|
59752
60023
|
function castFocus(pool){ const key="focuspool"; state.cast[key]=Math.min(pool,(state.cast[key]||0)+1); saveState(); renderToday(); }
|
|
59753
|
-
function refocus(){
|
|
60024
|
+
function refocus(){
|
|
60025
|
+
const key="focuspool"; state.cast[key]=Math.max(0,(state.cast[key]||0)-1);
|
|
60026
|
+
// Some resources (e.g. the psychic's Unleash Psyche) recharge when you Refocus.
|
|
60027
|
+
getFeatures("dailyResource").forEach(f=>{ if(f.recharge==="refocus") delete state.cast["resource:"+f.key]; });
|
|
60028
|
+
saveState(); renderToday(); toast("Refocused — +1 Focus Point");
|
|
60029
|
+
}
|
|
59754
60030
|
|
|
59755
60031
|
function renderTodayPrepared(p){
|
|
59756
60032
|
let html=todayHeaderHTML();
|
|
59757
60033
|
html+=dailyResourceHTML();
|
|
60034
|
+
html+=stageTrackerHTML();
|
|
59758
60035
|
|
|
59759
60036
|
if(p.divineFont){
|
|
59760
60037
|
const f=p.divineFont; const fontSpell=findSpell(f.fontSlug);
|
|
59761
|
-
html+=`<div class="sectionhead">${f.font==="heal"?"
|
|
60038
|
+
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
60039
|
html+=castableCard(fontSpell,"font",f.fontRank,f.fontCount,f.fontRank,0,{cls:f.font});
|
|
59763
60040
|
}
|
|
59764
60041
|
|
|
59765
60042
|
if(p.cantrips && p.cantrips.length){
|
|
59766
|
-
html+=`<div class="sectionhead"
|
|
60043
|
+
html+=`<div class="sectionhead">Cantrips <span class="count">unlimited</span></div><div class="divider"></div>`;
|
|
59767
60044
|
p.cantrips.forEach((slug,i)=>{ const s=findSpell(slug); if(!s) return; html+=castableCard(s,"cantrip",0,Infinity,0,i); });
|
|
59768
60045
|
}
|
|
59769
60046
|
|
|
@@ -59773,19 +60050,26 @@ function renderTodayPrepared(p){
|
|
|
59773
60050
|
const extras=[];
|
|
59774
60051
|
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
60052
|
if(!arr.length && !extras.length) return;
|
|
59776
|
-
html+=`<div class="sectionhead"
|
|
60053
|
+
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
60054
|
(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
60055
|
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
60056
|
});
|
|
60057
|
+
studiousFeats().forEach(f=>{
|
|
60058
|
+
const arr=((p.studious&&p.studious[f.key])||[]).filter(Boolean);
|
|
60059
|
+
if(!arr.length) return;
|
|
60060
|
+
html+=`<div class="sectionhead">${f.label} <span class="count">${arr.length} slot${arr.length>1?"s":""}</span></div><div class="divider"></div>`;
|
|
60061
|
+
arr.forEach((slug,i)=>{ const s=findSpell(slug); if(!s) return; html+=castableCard(s,"studious",f.rank,1,f.rank,i,{label:"school"}); });
|
|
60062
|
+
});
|
|
59780
60063
|
return html;
|
|
59781
60064
|
}
|
|
59782
60065
|
|
|
59783
60066
|
function renderTodaySpontaneous(p){
|
|
59784
60067
|
let html=todayHeaderHTML();
|
|
59785
60068
|
html+=dailyResourceHTML();
|
|
60069
|
+
html+=stageTrackerHTML();
|
|
59786
60070
|
|
|
59787
60071
|
if(p.cantrips && p.cantrips.length){
|
|
59788
|
-
html+=`<div class="sectionhead"
|
|
60072
|
+
html+=`<div class="sectionhead">Cantrips <span class="count">unlimited</span></div><div class="divider"></div>`;
|
|
59789
60073
|
p.cantrips.forEach((slug,i)=>{ const s=findSpell(slug); if(!s) return; html+=castableCard(s,"cantrip",0,Infinity,0,i); });
|
|
59790
60074
|
}
|
|
59791
60075
|
|
|
@@ -59802,7 +60086,7 @@ function renderTodaySpontaneous(p){
|
|
|
59802
60086
|
|
|
59803
60087
|
let pips=""; for(let k=0;k<max;k++){ pips+=`<span class="pip ${k<remaining?"full":"spent"}"></span>`; }
|
|
59804
60088
|
const undo = spent>0 ? `<button class="castbtn zero" style="margin-left:8px" onclick="uncastPool(${R})">↩</button>` : "";
|
|
59805
|
-
html+=`<div class="sectionhead"
|
|
60089
|
+
html+=`<div class="sectionhead">Rank ${R} <span class="count">${remaining}/${max} slot${max>1?"s":""} left</span></div>
|
|
59806
60090
|
<div style="margin:2px 0 8px"><span class="uses">${pips}${undo}</span></div><div class="divider"></div>`;
|
|
59807
60091
|
|
|
59808
60092
|
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 +60106,7 @@ function castableCard(s, kind, rank, uses, castRank, idx, opts){
|
|
|
59822
60106
|
const isSpent=!unlimited && remaining<=0;
|
|
59823
60107
|
const cls = opts.cls || (isSpent?"":cardClass(s));
|
|
59824
60108
|
const heightNote=(castRank>s.rank)?`<span class="meta"> · cast at rank ${castRank}</span>`:"";
|
|
59825
|
-
const tag = opts.label?`<span class="slottag">${opts.label}</span> `:"";
|
|
60109
|
+
const tag = opts.label?`<span class="slottag">${iconSvg(opts.label)}</span> `:"";
|
|
59826
60110
|
|
|
59827
60111
|
let usesHtml="";
|
|
59828
60112
|
if(unlimited){ usesHtml=`<span class="unlim">∞ at will</span>`; }
|
|
@@ -59841,7 +60125,7 @@ function castableCard(s, kind, rank, uses, castRank, idx, opts){
|
|
|
59841
60125
|
<div class="nm">${tag}${escapeHtml(s.name)} ${heightNote}</div>
|
|
59842
60126
|
<div class="uses">${usesHtml} ${btn} ${sustainBtn(s)}</div>
|
|
59843
60127
|
</div>
|
|
59844
|
-
<div class="meta">${actionLabel(s.actions)}${s.save?` ·
|
|
60128
|
+
<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
60129
|
<button class="detailsbtn" onclick="toggleDetails('${detailsId}',this)">Show details ▾</button>
|
|
59846
60130
|
<div id="${detailsId}" class="hide" style="margin-top:10px">${spellCardHTML(s,{cls})}</div>
|
|
59847
60131
|
</div>`;
|
|
@@ -59862,7 +60146,7 @@ function poolSpellCard(s, poolRank, castRank, remaining, max, uid, sig){
|
|
|
59862
60146
|
<div class="nm">${star}${escapeHtml(s.name)} ${heightNote}</div>
|
|
59863
60147
|
<div class="uses">${btn} ${sustainBtn(s)}</div>
|
|
59864
60148
|
</div>
|
|
59865
|
-
<div class="meta">${actionLabel(s.actions)}${s.save?` ·
|
|
60149
|
+
<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
60150
|
<button class="detailsbtn" onclick="toggleDetails('${detailsId}',this)">Show details ▾</button>
|
|
59867
60151
|
<div id="${detailsId}" class="hide" style="margin-top:10px">${spellCardHTML(s)}</div>
|
|
59868
60152
|
</div>`;
|
|
@@ -59873,22 +60157,22 @@ function doCast(key,max){ state.cast[key]=(state.cast[key]||0)+1; if(state.cast[
|
|
|
59873
60157
|
function uncast(key){ state.cast[key]=Math.max(0,(state.cast[key]||0)-1); saveState(); renderToday(); }
|
|
59874
60158
|
function castPool(rank,max){ const key="pool:"+rank; state.cast[key]=Math.min(max,(state.cast[key]||0)+1); saveState(); renderToday(); }
|
|
59875
60159
|
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("
|
|
60160
|
+
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
60161
|
|
|
59878
60162
|
/* ---- Sustained-spell tracker ---- */
|
|
59879
60163
|
function sustainBtn(s){
|
|
59880
60164
|
if(!s || !s.sustained) return "";
|
|
59881
60165
|
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}')"
|
|
60166
|
+
return on ? `<button class="susbtn on" onclick="endSustain('${s.slug}')">End</button>`
|
|
60167
|
+
: `<button class="susbtn" onclick="startSustain('${s.slug}')">Sustain</button>`;
|
|
59884
60168
|
}
|
|
59885
|
-
function startSustain(slug){ state.sustaining=state.sustaining||[]; if(!state.sustaining.includes(slug)){ state.sustaining.push(slug); saveState(); renderToday(); toast("
|
|
60169
|
+
function startSustain(slug){ state.sustaining=state.sustaining||[]; if(!state.sustaining.includes(slug)){ state.sustaining.push(slug); saveState(); renderToday(); toast("Now sustaining"); } }
|
|
59886
60170
|
function endSustain(slug){ state.sustaining=(state.sustaining||[]).filter(x=>x!==slug); saveState(); renderToday(); }
|
|
59887
60171
|
function sustainingBarHTML(){
|
|
59888
60172
|
const list=(state.sustaining||[]).filter(slug=>findSpell(slug));
|
|
59889
60173
|
if(!list.length) return "";
|
|
59890
|
-
return `<div class="card" style="border-left:
|
|
59891
|
-
<div class="sectionhead" style="margin:0 0 6px"
|
|
60174
|
+
return `<div class="card" style="border-left:5px solid var(--info)">
|
|
60175
|
+
<div class="sectionhead" style="margin:0 0 6px">Sustaining now <span class="count">spend an action each turn</span></div>
|
|
59892
60176
|
${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
60177
|
</div>`;
|
|
59894
60178
|
}
|
|
@@ -59931,8 +60215,8 @@ function renderBrowseFilters(){
|
|
|
59931
60215
|
function renderRankChips(){
|
|
59932
60216
|
const ranks=[["all","All"],["0","Cantrips"]];
|
|
59933
60217
|
for(let r=1;r<=10;r++) ranks.push([String(r),"Rank "+r]);
|
|
59934
|
-
if(hasFocus()) ranks.push(["focus","
|
|
59935
|
-
ranks.push(["rituals","
|
|
60218
|
+
if(hasFocus()) ranks.push(["focus","Focus"]);
|
|
60219
|
+
ranks.push(["rituals","Rituals"]);
|
|
59936
60220
|
document.getElementById("rankChips").innerHTML=
|
|
59937
60221
|
ranks.map(([v,l])=>`<button class="chip ${browseRank===v?"on":""}" onclick="setBrowseRank('${v}')">${l}</button>`).join("");
|
|
59938
60222
|
}
|
|
@@ -59967,7 +60251,7 @@ function renderBrowse(){
|
|
|
59967
60251
|
}
|
|
59968
60252
|
/* Lightweight collapsed row; the full card renders on demand when tapped. */
|
|
59969
60253
|
function browseRowHTML(s){
|
|
59970
|
-
const save=s.save?` ·
|
|
60254
|
+
const save=s.save?` · ${escapeHtml(s.save)}`:"";
|
|
59971
60255
|
const note=legacyNote(s)?` <span class="formerly">${escapeHtml(legacyNote(s))}</span>`:"";
|
|
59972
60256
|
return `<div class="browse-row rank${s.rank}">
|
|
59973
60257
|
<button class="browse-row-head" onclick="toggleBrowse('${s.slug}',this)">
|
|
@@ -60078,6 +60362,14 @@ function downloadOffline(){
|
|
|
60078
60362
|
|
|
60079
60363
|
function init(){
|
|
60080
60364
|
setupInstall();
|
|
60365
|
+
applyTheme();
|
|
60366
|
+
try{
|
|
60367
|
+
if(window.matchMedia){
|
|
60368
|
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{
|
|
60369
|
+
if(((library.settings&&library.settings.themeMode)||"auto")==="auto") applyTheme();
|
|
60370
|
+
});
|
|
60371
|
+
}
|
|
60372
|
+
}catch(e){}
|
|
60081
60373
|
if(!state.classId){ showClassPicker(); }
|
|
60082
60374
|
else { renderHeader(); go(state.prepared?"today":"prepare"); }
|
|
60083
60375
|
}
|