pf2e-primer 0.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/LICENSE +31 -0
- package/README.md +106 -0
- package/bin/cli.mjs +86 -0
- package/data/reference.generated.js +6 -0
- package/dist/icon.svg +6 -0
- package/dist/index.html +2275 -0
- package/dist/manifest.webmanifest +13 -0
- package/dist/sw.js +60 -0
- package/notice.md +124 -0
- package/package.json +47 -0
- package/src/content.js +547 -0
- package/src/engine.js +1210 -0
- package/src/icon.svg +6 -0
- package/src/manifest.webmanifest +13 -0
- package/src/styles.css +445 -0
- package/src/sw.js +60 -0
- package/src/template.html +67 -0
- package/tools/build.py +60 -0
- package/tools/build_reference.py +249 -0
- package/tools/test.mjs +277 -0
package/src/icon.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#3f7d8c" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<circle cx="12" cy="5" r="2"/>
|
|
3
|
+
<path d="M12 7v13"/>
|
|
4
|
+
<path d="M8 11h8"/>
|
|
5
|
+
<path d="M5 14a7 7 0 0 0 14 0"/>
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "PF2e Primer",
|
|
3
|
+
"short_name": "PF2e Primer",
|
|
4
|
+
"description": "A hands-on introduction to Pathfinder 2e: the three pillars of play, with live demos of every core rule.",
|
|
5
|
+
"start_url": ".",
|
|
6
|
+
"scope": ".",
|
|
7
|
+
"display": "standalone",
|
|
8
|
+
"background_color": "#14161b",
|
|
9
|
+
"theme_color": "#14161b",
|
|
10
|
+
"icons": [
|
|
11
|
+
{ "src": "icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable" }
|
|
12
|
+
]
|
|
13
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
THEME TOKENS — base tokens flip with [data-theme]; --accent is
|
|
3
|
+
set by JS. Shades derive with color-mix() so one colour drives
|
|
4
|
+
the whole page. Same token names as the party tracker and the
|
|
5
|
+
spellbook, so the three tools look like a set.
|
|
6
|
+
============================================================ */
|
|
7
|
+
:root{
|
|
8
|
+
--accent:#3f7d8c;
|
|
9
|
+
--accent-ink:#ffffff;
|
|
10
|
+
--accent-dim:color-mix(in srgb, var(--accent) 55%, var(--line));
|
|
11
|
+
--accent-soft:color-mix(in srgb, var(--accent) 14%, transparent);
|
|
12
|
+
--accent-text:var(--accent);
|
|
13
|
+
--radius:14px;
|
|
14
|
+
}
|
|
15
|
+
:root, :root[data-theme="dark"]{
|
|
16
|
+
color-scheme:dark;
|
|
17
|
+
--bg:#14161b; --surface:#1d2027; --surface-2:#262a33;
|
|
18
|
+
--ink:#e8eaf0; --muted:#9aa1b0; --line:#2f3440;
|
|
19
|
+
--heal:#5bbf7e; --harm:#d9637a; --info:#6fa8dc; --warn:#d6a94a;
|
|
20
|
+
--accent-text:color-mix(in srgb, var(--accent) 78%, #ffffff);
|
|
21
|
+
--shadow:0 1px 2px rgba(0,0,0,.4), 0 4px 14px rgba(0,0,0,.22);
|
|
22
|
+
}
|
|
23
|
+
:root[data-theme="light"]{
|
|
24
|
+
color-scheme:light;
|
|
25
|
+
--bg:#f4f5f8; --surface:#ffffff; --surface-2:#eef0f4;
|
|
26
|
+
--ink:#1b1e24; --muted:#5d6470; --line:#dfe3ea;
|
|
27
|
+
--heal:#2f9e5b; --harm:#c43a57; --info:#2b6cb0; --warn:#9a6b12;
|
|
28
|
+
--accent-text:color-mix(in srgb, var(--accent) 72%, #000);
|
|
29
|
+
--shadow:0 1px 2px rgba(20,30,50,.08), 0 4px 14px rgba(20,30,50,.06);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
|
|
33
|
+
html,body{margin:0;padding:0;background:var(--bg);}
|
|
34
|
+
body{
|
|
35
|
+
color:var(--ink);
|
|
36
|
+
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
|
37
|
+
font-size:16px; line-height:1.55; -webkit-text-size-adjust:100%;
|
|
38
|
+
}
|
|
39
|
+
h1,h2,h3{margin:0 0 .4em;line-height:1.2;letter-spacing:-.01em;}
|
|
40
|
+
h1{font-size:1.5rem;font-weight:800;}
|
|
41
|
+
h2{font-size:1.12rem;font-weight:800;}
|
|
42
|
+
h3{font-size:.98rem;font-weight:800;}
|
|
43
|
+
p{margin:.55em 0;}
|
|
44
|
+
b,strong{font-weight:700;}
|
|
45
|
+
i,em{font-style:italic;}
|
|
46
|
+
a{color:var(--accent-text);}
|
|
47
|
+
.hide{display:none !important;}
|
|
48
|
+
.center{text-align:center;}
|
|
49
|
+
.icn{display:inline-block;width:1em;height:1em;flex:0 0 auto;vertical-align:-.14em;}
|
|
50
|
+
button{font-family:inherit;cursor:pointer;}
|
|
51
|
+
button:disabled{cursor:default;}
|
|
52
|
+
button,.chip,input,select,textarea,details.ref,.pbtn{transition:background-color .15s ease,border-color .15s ease,color .15s ease;}
|
|
53
|
+
|
|
54
|
+
/* ---- Header ---- */
|
|
55
|
+
header.top{
|
|
56
|
+
background:linear-gradient(180deg, color-mix(in srgb, var(--accent) 10%, var(--surface)), var(--surface));
|
|
57
|
+
border-bottom:1px solid var(--line);
|
|
58
|
+
padding:12px 20px 0; position:relative; z-index:30;
|
|
59
|
+
}
|
|
60
|
+
.tophead{display:flex;align-items:center;justify-content:space-between;gap:10px;max-width:1000px;margin:0 auto;}
|
|
61
|
+
header.top h1{margin:0;display:flex;align-items:center;gap:9px;font-size:1.22rem;}
|
|
62
|
+
.apptitle-ic{display:inline-flex;color:var(--accent-text);width:1.3rem;height:1.3rem;}
|
|
63
|
+
header.top .sub{color:var(--muted);font-size:.82rem;margin:3px auto 0;max-width:1000px;}
|
|
64
|
+
.headbtns{display:flex;gap:8px;flex:0 0 auto;}
|
|
65
|
+
.menubtn{
|
|
66
|
+
background:var(--surface-2);border:1px solid var(--line);border-radius:11px;
|
|
67
|
+
height:40px;min-width:40px;padding:0 12px;color:var(--muted);
|
|
68
|
+
display:inline-flex;align-items:center;justify-content:center;gap:7px;font-weight:800;font-size:.85rem;
|
|
69
|
+
}
|
|
70
|
+
.menubtn .icn{width:1.15rem;height:1.15rem;}
|
|
71
|
+
.menubtn.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
|
|
72
|
+
.progress{height:3px;background:var(--surface-2);max-width:1000px;margin:10px auto 0;border-radius:2px;overflow:hidden;}
|
|
73
|
+
.progress span{display:block;height:100%;background:var(--accent);transition:width .3s ease;}
|
|
74
|
+
|
|
75
|
+
/* ---- Tabs ---- */
|
|
76
|
+
nav.tabs{
|
|
77
|
+
position:sticky;top:0;z-index:20;background:var(--surface);border-bottom:1px solid var(--line);
|
|
78
|
+
display:flex;gap:2px;justify-content:center;padding:0 8px;overflow-x:auto;scrollbar-width:none;
|
|
79
|
+
}
|
|
80
|
+
nav.tabs::-webkit-scrollbar{display:none;}
|
|
81
|
+
nav.tabs button{
|
|
82
|
+
background:none;border:none;color:var(--muted);font-family:inherit;font-weight:700;font-size:.9rem;
|
|
83
|
+
padding:12px 14px;display:inline-flex;align-items:center;gap:7px;white-space:nowrap;
|
|
84
|
+
border-bottom:2px solid transparent;
|
|
85
|
+
}
|
|
86
|
+
nav.tabs button .icn{width:1.05rem;height:1.05rem;}
|
|
87
|
+
nav.tabs button:hover{color:var(--ink);}
|
|
88
|
+
nav.tabs button.on{color:var(--accent-text);border-bottom-color:var(--accent);}
|
|
89
|
+
|
|
90
|
+
/* ---- Layout ---- */
|
|
91
|
+
.wrap{max-width:1000px;margin:0 auto;padding:22px 20px 70px;}
|
|
92
|
+
.chaphead{margin-bottom:22px;}
|
|
93
|
+
.chaphead h1{display:flex;align-items:center;gap:11px;}
|
|
94
|
+
.chaphead h1 .icn{color:var(--accent-text);width:1.5rem;height:1.5rem;}
|
|
95
|
+
.lede{color:var(--muted);font-size:1.02rem;max-width:62ch;margin:0;}
|
|
96
|
+
.sec{margin:0 0 26px;}
|
|
97
|
+
.sec p{max-width:68ch;}
|
|
98
|
+
.meta{font-size:.9rem;color:var(--muted);}
|
|
99
|
+
.meta b{color:var(--ink);font-weight:600;}
|
|
100
|
+
.hintline{font-size:.88rem;color:var(--accent-text);display:flex;gap:7px;align-items:flex-start;margin-top:10px;}
|
|
101
|
+
.hintline .icn{margin-top:.25em;}
|
|
102
|
+
.empty{text-align:center;color:var(--muted);padding:34px 20px;}
|
|
103
|
+
|
|
104
|
+
/* ---- Callouts ---- */
|
|
105
|
+
.callout{
|
|
106
|
+
border:1px solid var(--line);border-left:4px solid var(--accent);border-radius:11px;
|
|
107
|
+
background:var(--surface);padding:14px 18px;margin:0 0 26px;box-shadow:var(--shadow);
|
|
108
|
+
}
|
|
109
|
+
.callout.warn{border-left-color:var(--warn);background:color-mix(in srgb,var(--warn) 7%,var(--surface));}
|
|
110
|
+
.callout.tip{border-left-color:var(--accent);}
|
|
111
|
+
.callout p{margin:.3em 0;max-width:66ch;}
|
|
112
|
+
|
|
113
|
+
/* ---- Bridge table ---- */
|
|
114
|
+
.bridge{border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;background:var(--surface);box-shadow:var(--shadow);}
|
|
115
|
+
.brow{display:grid;grid-template-columns:1fr 34px 1.35fr;gap:12px;align-items:center;padding:12px 16px;border-bottom:1px solid var(--line);}
|
|
116
|
+
.brow:last-child{border-bottom:none;}
|
|
117
|
+
.brow:nth-child(odd){background:color-mix(in srgb,var(--surface-2) 55%,var(--surface));}
|
|
118
|
+
.bfrom{color:var(--muted);font-size:.93rem;}
|
|
119
|
+
.barrow{display:flex;justify-content:center;color:var(--accent-text);}
|
|
120
|
+
.barrow .icn{width:1.05rem;height:1.05rem;}
|
|
121
|
+
.bto{font-size:.95rem;}
|
|
122
|
+
|
|
123
|
+
/* ---- Pillar → mode map ---- */
|
|
124
|
+
.pmap{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:12px;}
|
|
125
|
+
.pcol{
|
|
126
|
+
display:flex;flex-direction:column;gap:3px;text-align:left;
|
|
127
|
+
background:var(--surface);border:1px solid var(--line);border-top:3px solid var(--accent);
|
|
128
|
+
border-radius:12px;padding:14px 16px;color:var(--ink);box-shadow:var(--shadow);
|
|
129
|
+
}
|
|
130
|
+
.pcol:hover{border-color:var(--accent-dim);}
|
|
131
|
+
.pcol.wide{width:100%;border-top-color:var(--muted);}
|
|
132
|
+
.ppillar{font-size:.7rem;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);font-weight:800;}
|
|
133
|
+
.pmode{font-size:1.02rem;font-weight:800;color:var(--accent-text);}
|
|
134
|
+
.pgrain{font-size:.8rem;color:var(--muted);font-variant-numeric:tabular-nums;}
|
|
135
|
+
.pnote{font-size:.88rem;margin-top:5px;}
|
|
136
|
+
|
|
137
|
+
/* ---- Concept cards ---- */
|
|
138
|
+
.cardgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(215px,1fr));gap:12px;}
|
|
139
|
+
.ccard{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:14px 16px;box-shadow:var(--shadow);}
|
|
140
|
+
.ccard h3{color:var(--accent-text);margin-bottom:.25em;}
|
|
141
|
+
.ccard p{font-size:.92rem;margin:0;}
|
|
142
|
+
|
|
143
|
+
/* ---- Steps ---- */
|
|
144
|
+
ol.steps{margin:0;padding-left:0;counter-reset:step;list-style:none;}
|
|
145
|
+
ol.steps li{
|
|
146
|
+
counter-increment:step;position:relative;padding:9px 0 9px 42px;border-bottom:1px dotted var(--line);font-size:.95rem;
|
|
147
|
+
}
|
|
148
|
+
ol.steps li:last-child{border-bottom:none;}
|
|
149
|
+
ol.steps li::before{
|
|
150
|
+
content:counter(step);position:absolute;left:0;top:8px;
|
|
151
|
+
width:26px;height:26px;border-radius:50%;background:var(--accent-soft);color:var(--accent-text);
|
|
152
|
+
border:1px solid var(--accent-dim);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:.82rem;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ---- GM notes ---- */
|
|
156
|
+
.gmsec{display:none;}
|
|
157
|
+
body.gmon .gmsec{
|
|
158
|
+
display:block;background:color-mix(in srgb,var(--info) 8%,var(--surface));
|
|
159
|
+
border:1px dashed color-mix(in srgb,var(--info) 45%,var(--line));border-radius:var(--radius);
|
|
160
|
+
padding:14px 18px;margin-bottom:26px;
|
|
161
|
+
}
|
|
162
|
+
.gmhead{display:flex;align-items:center;gap:8px;font-weight:800;color:var(--info);font-size:.75rem;text-transform:uppercase;letter-spacing:.8px;margin-bottom:6px;}
|
|
163
|
+
.gmsec p{font-size:.93rem;margin:.4em 0;max-width:70ch;}
|
|
164
|
+
|
|
165
|
+
/* ---- Tool links ---- */
|
|
166
|
+
.toolcard{
|
|
167
|
+
display:flex;flex-direction:column;gap:3px;text-decoration:none;color:var(--ink);
|
|
168
|
+
background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:13px 16px;margin:8px 0;box-shadow:var(--shadow);
|
|
169
|
+
}
|
|
170
|
+
.toolcard:hover{border-color:var(--accent-dim);}
|
|
171
|
+
.tname{font-weight:800;color:var(--accent-text);display:flex;align-items:center;gap:8px;}
|
|
172
|
+
.tnote{font-size:.88rem;color:var(--muted);}
|
|
173
|
+
|
|
174
|
+
/* ---- Reference disclosure ---- */
|
|
175
|
+
details.ref{border:1px solid var(--line);border-radius:11px;margin:8px 0;background:var(--surface);overflow:hidden;}
|
|
176
|
+
details.ref summary{padding:11px 14px;cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between;gap:10px;}
|
|
177
|
+
details.ref summary::-webkit-details-marker{display:none;}
|
|
178
|
+
details.ref[open]{border-color:var(--accent-dim);}
|
|
179
|
+
.refname{font-weight:700;}
|
|
180
|
+
.refkind{font-size:.68rem;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);background:var(--surface-2);border:1px solid var(--line);border-radius:6px;padding:1px 7px;white-space:nowrap;}
|
|
181
|
+
.refbody{padding:0 14px 14px;font-size:.92rem;line-height:1.6;}
|
|
182
|
+
|
|
183
|
+
/* ---- Search + chips ---- */
|
|
184
|
+
.searchbar{display:flex;align-items:center;gap:8px;background:var(--surface-2);border:1px solid var(--line);border-radius:10px;padding:0 12px;margin-bottom:12px;}
|
|
185
|
+
.searchbar .icn{color:var(--muted);width:1.1rem;height:1.1rem;}
|
|
186
|
+
.searchbar input{border:none;background:none;padding:11px 0;}
|
|
187
|
+
.searchbar input:focus{border:none;}
|
|
188
|
+
.chips{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0;}
|
|
189
|
+
.chip{
|
|
190
|
+
background:var(--surface);border:1px solid var(--line);color:var(--muted);
|
|
191
|
+
border-radius:8px;padding:6px 11px;font-size:.84rem;font-weight:700;
|
|
192
|
+
}
|
|
193
|
+
.chip.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
|
|
194
|
+
|
|
195
|
+
/* ---- Quick card ---- */
|
|
196
|
+
.qcard{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;}
|
|
197
|
+
.qbox{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:14px 16px;box-shadow:var(--shadow);}
|
|
198
|
+
.qbox h3{color:var(--accent-text);text-transform:uppercase;font-size:.72rem;letter-spacing:.7px;}
|
|
199
|
+
.qbox .big{font-size:1.12rem;font-weight:800;margin:.2em 0 .5em;font-variant-numeric:tabular-nums;}
|
|
200
|
+
.qbox ul{margin:0;padding-left:18px;font-size:.89rem;}
|
|
201
|
+
.qbox li{margin:.25em 0;}
|
|
202
|
+
|
|
203
|
+
/* ---- Glossary ---- */
|
|
204
|
+
dl.gloss{margin:0;display:grid;grid-template-columns:minmax(140px,190px) 1fr;gap:0;border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;background:var(--surface);}
|
|
205
|
+
dl.gloss dt{font-weight:800;color:var(--accent-text);padding:11px 14px;border-bottom:1px solid var(--line);font-size:.92rem;}
|
|
206
|
+
dl.gloss dd{margin:0;padding:11px 14px;border-bottom:1px solid var(--line);font-size:.92rem;}
|
|
207
|
+
dl.gloss dt:nth-last-of-type(1),dl.gloss dd:nth-last-of-type(1){border-bottom:none;}
|
|
208
|
+
|
|
209
|
+
/* ============================================================
|
|
210
|
+
DEMOS
|
|
211
|
+
============================================================ */
|
|
212
|
+
.demo{
|
|
213
|
+
background:var(--surface);border:1px solid var(--accent-dim);border-radius:var(--radius);
|
|
214
|
+
padding:16px 18px;box-shadow:var(--shadow);
|
|
215
|
+
}
|
|
216
|
+
.demohead{display:flex;align-items:center;gap:9px;color:var(--accent-text);}
|
|
217
|
+
.demohead h2{margin:0;color:var(--ink);}
|
|
218
|
+
.demohead .icn{width:1.2rem;height:1.2rem;}
|
|
219
|
+
.demobody{margin-top:12px;}
|
|
220
|
+
.controls{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end;margin-bottom:12px;}
|
|
221
|
+
label.field{display:block;margin:0;}
|
|
222
|
+
label.field.inline{flex:1;min-width:180px;}
|
|
223
|
+
label.field.narrow{flex:0 0 120px;min-width:120px;}
|
|
224
|
+
label.field .name{display:block;font-weight:700;margin-bottom:5px;font-size:.8rem;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);}
|
|
225
|
+
input,select,textarea{
|
|
226
|
+
width:100%;padding:10px;font-size:.98rem;border-radius:10px;font-family:inherit;
|
|
227
|
+
background:var(--surface-2);color:var(--ink);border:1px solid var(--line);appearance:none;-webkit-appearance:none;accent-color:var(--accent);
|
|
228
|
+
}
|
|
229
|
+
input:focus,select:focus{outline:none;border-color:var(--accent);}
|
|
230
|
+
select{background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);
|
|
231
|
+
background-position:calc(100% - 20px) center,calc(100% - 14px) center;background-size:6px 6px,6px 6px;background-repeat:no-repeat;padding-right:38px;}
|
|
232
|
+
input[type="color"]{padding:4px;height:42px;cursor:pointer;}
|
|
233
|
+
input[type="number"]{-moz-appearance:textfield;}
|
|
234
|
+
.row{display:flex;gap:12px;}
|
|
235
|
+
.row.tight{gap:8px;align-items:center;flex-wrap:wrap;}
|
|
236
|
+
.btn{
|
|
237
|
+
display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:12px;border-radius:11px;border:none;
|
|
238
|
+
background:var(--accent);color:var(--accent-ink);font-weight:800;font-size:1rem;margin:12px 0;box-shadow:var(--shadow);
|
|
239
|
+
}
|
|
240
|
+
.btn.secondary{background:var(--surface-2);color:var(--accent-text);border:1px solid var(--accent-dim);box-shadow:none;}
|
|
241
|
+
.btn.sm{width:auto;padding:9px 15px;font-size:.9rem;margin:0;border-radius:9px;}
|
|
242
|
+
.btn:disabled{opacity:.45;}
|
|
243
|
+
.btn:active{transform:translateY(1px);}
|
|
244
|
+
.chapfoot{border-top:1px solid var(--line);padding-top:18px;margin-top:34px;}
|
|
245
|
+
.chapfoot .btn{max-width:420px;margin:0 auto 8px;}
|
|
246
|
+
|
|
247
|
+
/* ---- Foe / NPC card ---- */
|
|
248
|
+
.foecard{
|
|
249
|
+
flex:1;min-width:230px;display:flex;flex-direction:column;gap:2px;
|
|
250
|
+
background:var(--surface-2);border:1px solid var(--line);border-radius:10px;padding:9px 13px;
|
|
251
|
+
}
|
|
252
|
+
.foecard .meta{font-size:.8rem;}
|
|
253
|
+
|
|
254
|
+
/* ---- Result card ---- */
|
|
255
|
+
.out{margin-top:12px;}
|
|
256
|
+
.resultcard{
|
|
257
|
+
background:var(--surface-2);border:1px solid var(--line);border-left:4px solid var(--line);
|
|
258
|
+
border-radius:11px;padding:14px 16px;
|
|
259
|
+
}
|
|
260
|
+
.resultcard.csucc,.resultcard.succ{border-left-color:var(--heal);}
|
|
261
|
+
.resultcard.fail{border-left-color:var(--warn);}
|
|
262
|
+
.resultcard.cfail{border-left-color:var(--harm);}
|
|
263
|
+
.rline{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap;}
|
|
264
|
+
.rmath{color:var(--muted);font-size:.9rem;font-variant-numeric:tabular-nums;}
|
|
265
|
+
.rtotal{font-size:1.7rem;font-weight:800;font-variant-numeric:tabular-nums;}
|
|
266
|
+
.rdeg{margin:8px 0;}
|
|
267
|
+
.rwhy{font-size:.93rem;margin:.35em 0;}
|
|
268
|
+
.nat{font-weight:800;padding:0 4px;border-radius:5px;}
|
|
269
|
+
.nat.nat20{color:var(--heal);background:color-mix(in srgb,var(--heal) 20%,transparent);}
|
|
270
|
+
.nat.nat1{color:var(--harm);background:color-mix(in srgb,var(--harm) 20%,transparent);}
|
|
271
|
+
.deg{
|
|
272
|
+
display:inline-block;font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.4px;
|
|
273
|
+
padding:3px 9px;border-radius:6px;
|
|
274
|
+
}
|
|
275
|
+
.deg.csucc{background:color-mix(in srgb,var(--heal) 24%,var(--surface));color:var(--heal);}
|
|
276
|
+
.deg.succ{background:color-mix(in srgb,var(--heal) 13%,var(--surface));color:var(--heal);}
|
|
277
|
+
.deg.fail{background:color-mix(in srgb,var(--warn) 17%,var(--surface));color:var(--warn);}
|
|
278
|
+
.deg.cfail{background:color-mix(in srgb,var(--harm) 19%,var(--surface));color:var(--harm);}
|
|
279
|
+
|
|
280
|
+
/* ---- Distribution bars ---- */
|
|
281
|
+
.dist{display:flex;flex-direction:column;gap:5px;margin:10px 0;}
|
|
282
|
+
.distrow{display:grid;grid-template-columns:130px 1fr 28px;gap:10px;align-items:center;font-size:.85rem;}
|
|
283
|
+
.dlbl{color:var(--muted);}
|
|
284
|
+
.dbar{height:12px;background:var(--surface);border:1px solid var(--line);border-radius:7px;overflow:hidden;}
|
|
285
|
+
.dfill{display:block;height:100%;}
|
|
286
|
+
.dfill.csucc{background:var(--heal);}
|
|
287
|
+
.dfill.succ{background:color-mix(in srgb,var(--heal) 60%,var(--surface));}
|
|
288
|
+
.dfill.fail{background:var(--warn);}
|
|
289
|
+
.dfill.cfail{background:var(--harm);}
|
|
290
|
+
.dnum{text-align:right;font-weight:800;font-variant-numeric:tabular-nums;}
|
|
291
|
+
|
|
292
|
+
/* ---- Action pips & palette ---- */
|
|
293
|
+
.pips{display:flex;gap:8px;margin:4px 0 8px;flex-wrap:wrap;}
|
|
294
|
+
.pip{
|
|
295
|
+
flex:1;min-width:120px;min-height:52px;border:1px dashed var(--line);border-radius:11px;
|
|
296
|
+
background:var(--surface-2);display:flex;align-items:center;justify-content:center;padding:6px 10px;text-align:center;
|
|
297
|
+
}
|
|
298
|
+
.pip.used{border-style:solid;border-color:var(--accent);background:var(--accent-soft);}
|
|
299
|
+
.pip.react{flex:0 0 110px;border-color:color-mix(in srgb,var(--info) 40%,var(--line));}
|
|
300
|
+
.pipname{font-weight:700;font-size:.85rem;}
|
|
301
|
+
.pipname.empty{color:var(--muted);font-weight:600;}
|
|
302
|
+
.palette{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:8px;margin:10px 0;}
|
|
303
|
+
.pbtn{
|
|
304
|
+
display:flex;flex-direction:column;align-items:flex-start;gap:2px;text-align:left;
|
|
305
|
+
background:var(--surface-2);border:1px solid var(--line);border-left:3px solid var(--line);
|
|
306
|
+
border-radius:10px;padding:9px 12px;color:var(--ink);
|
|
307
|
+
}
|
|
308
|
+
.pbtn:hover:not(:disabled){border-color:var(--accent-dim);border-left-color:var(--accent);}
|
|
309
|
+
.pbtn.dis,.pbtn:disabled{opacity:.4;}
|
|
310
|
+
.pbtn.k-attack{border-left-color:var(--harm);}
|
|
311
|
+
.pbtn.k-spell{border-left-color:var(--info);}
|
|
312
|
+
.pbtn.k-skill{border-left-color:var(--accent);}
|
|
313
|
+
.pbtn.k-defend{border-left-color:var(--heal);}
|
|
314
|
+
.pbtn.k-move{border-left-color:var(--muted);}
|
|
315
|
+
.pcost{font-size:.72rem;color:var(--accent-text);letter-spacing:1px;}
|
|
316
|
+
.plabel{font-weight:700;font-size:.9rem;line-height:1.25;}
|
|
317
|
+
.pmap{font-size:.75rem;color:var(--muted);font-variant-numeric:tabular-nums;}
|
|
318
|
+
.pblock{font-size:.72rem;color:var(--harm);font-weight:700;}
|
|
319
|
+
|
|
320
|
+
/* ---- Turn / event log ---- */
|
|
321
|
+
ol.turnlog{list-style:none;margin:12px 0 0;padding:0;display:flex;flex-direction:column;gap:8px;}
|
|
322
|
+
.tstep{
|
|
323
|
+
background:var(--surface-2);border:1px solid var(--line);border-left:4px solid var(--line);
|
|
324
|
+
border-radius:10px;padding:10px 13px;
|
|
325
|
+
}
|
|
326
|
+
.tstep.csucc,.tstep.succ{border-left-color:var(--heal);}
|
|
327
|
+
.tstep.fail{border-left-color:var(--warn);}
|
|
328
|
+
.tstep.cfail{border-left-color:var(--harm);}
|
|
329
|
+
.tshead{display:flex;align-items:center;gap:9px;font-weight:800;flex-wrap:wrap;margin-bottom:3px;}
|
|
330
|
+
.tsmath{font-size:.88rem;color:var(--muted);font-variant-numeric:tabular-nums;}
|
|
331
|
+
.tswhy{font-size:.85rem;color:var(--muted);font-style:italic;margin-top:2px;}
|
|
332
|
+
.tsdetail{font-size:.92rem;margin-top:5px;}
|
|
333
|
+
.turnsum{
|
|
334
|
+
margin-top:12px;padding:12px 14px;border-radius:10px;
|
|
335
|
+
background:var(--accent-soft);border:1px solid var(--accent-dim);font-size:.95rem;
|
|
336
|
+
}
|
|
337
|
+
.turnsum .meta{display:block;margin-top:4px;}
|
|
338
|
+
|
|
339
|
+
/* ---- Dying tracker ---- */
|
|
340
|
+
.dyhead{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}
|
|
341
|
+
.hpbar{flex:1;min-width:140px;max-width:280px;height:10px;border-radius:6px;background:var(--surface-2);border:1px solid var(--line);overflow:hidden;}
|
|
342
|
+
.hpfill{display:block;height:100%;background:var(--heal);transition:width .25s ease;}
|
|
343
|
+
.hpfill.mid{background:var(--warn);}
|
|
344
|
+
.hpfill.low{background:var(--harm);}
|
|
345
|
+
.hpnum{font-weight:800;font-variant-numeric:tabular-nums;}
|
|
346
|
+
.dychips{display:flex;gap:7px;flex-wrap:wrap;margin:10px 0;}
|
|
347
|
+
.statchip{
|
|
348
|
+
font-size:.78rem;font-weight:800;border-radius:7px;padding:3px 9px;
|
|
349
|
+
background:var(--surface-2);border:1px solid var(--line);color:var(--muted);
|
|
350
|
+
}
|
|
351
|
+
.statchip.bad{background:color-mix(in srgb,var(--harm) 17%,var(--surface));border-color:color-mix(in srgb,var(--harm) 40%,var(--line));color:var(--harm);}
|
|
352
|
+
.statchip.warn{background:color-mix(in srgb,var(--warn) 15%,var(--surface));border-color:color-mix(in srgb,var(--warn) 40%,var(--line));color:var(--warn);}
|
|
353
|
+
.statchip.good{background:color-mix(in srgb,var(--heal) 14%,var(--surface));border-color:color-mix(in srgb,var(--heal) 40%,var(--line));color:var(--heal);}
|
|
354
|
+
|
|
355
|
+
/* ---- Exploration / downtime board ---- */
|
|
356
|
+
.exphead,.exprow{display:grid;grid-template-columns:76px 110px 1.1fr 110px 1.5fr;gap:12px;align-items:center;}
|
|
357
|
+
.exphead{font-size:.7rem;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);font-weight:800;padding:0 0 8px;border-bottom:1px solid var(--line);}
|
|
358
|
+
.exprow{padding:10px 0;border-bottom:1px solid var(--line);}
|
|
359
|
+
.exprow.down{grid-template-columns:110px 1.1fr 150px 1.5fr;}
|
|
360
|
+
.expord{display:flex;align-items:center;gap:4px;}
|
|
361
|
+
.ordbtn{background:var(--surface-2);border:1px solid var(--line);color:var(--muted);border-radius:7px;padding:4px;display:inline-flex;}
|
|
362
|
+
.ordbtn:disabled{opacity:.35;}
|
|
363
|
+
.ordbtn .icn{width:.95rem;height:.95rem;}
|
|
364
|
+
.ordnum{font-weight:800;color:var(--accent-text);min-width:14px;text-align:center;}
|
|
365
|
+
.expwho{display:flex;flex-direction:column;}
|
|
366
|
+
.expwho .meta{font-size:.78rem;}
|
|
367
|
+
.expsel select{padding:8px;font-size:.9rem;}
|
|
368
|
+
.govmod{font-size:1.05rem;color:var(--accent-text);}
|
|
369
|
+
.expnote{font-size:.86rem;color:var(--muted);}
|
|
370
|
+
.secretchip{
|
|
371
|
+
display:inline-flex;align-items:center;gap:4px;font-size:.72rem;font-weight:700;white-space:nowrap;
|
|
372
|
+
background:color-mix(in srgb,var(--info) 12%,var(--surface));border:1px solid color-mix(in srgb,var(--info) 40%,var(--line));
|
|
373
|
+
color:var(--info);border-radius:6px;padding:1px 7px;
|
|
374
|
+
}
|
|
375
|
+
.secretchip .icn{width:.85rem;height:.85rem;}
|
|
376
|
+
ul.notelist{margin:14px 0 0;padding-left:18px;font-size:.9rem;}
|
|
377
|
+
ul.notelist li{margin:.35em 0;}
|
|
378
|
+
|
|
379
|
+
/* ---- Attitude ladder ---- */
|
|
380
|
+
.ladder{display:flex;flex-direction:column;gap:4px;margin:12px 0;}
|
|
381
|
+
.rung{
|
|
382
|
+
display:flex;align-items:center;justify-content:space-between;gap:10px;
|
|
383
|
+
border:1px solid var(--line);border-radius:9px;padding:8px 13px;background:var(--surface-2);color:var(--muted);font-weight:700;font-size:.9rem;
|
|
384
|
+
}
|
|
385
|
+
.rung.below{opacity:.65;}
|
|
386
|
+
.rung.on{background:var(--accent-soft);border-color:var(--accent);color:var(--accent-text);}
|
|
387
|
+
.rhere{font-size:.78rem;font-weight:800;}
|
|
388
|
+
|
|
389
|
+
/* ---- Menu ---- */
|
|
390
|
+
.seg{display:flex;gap:8px;margin:6px 0 12px;}
|
|
391
|
+
.seg button{flex:1;padding:11px;border-radius:10px;border:1px solid var(--line);background:var(--surface-2);color:var(--ink);font-weight:700;}
|
|
392
|
+
.seg button.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
|
|
393
|
+
.swatchrow{display:flex;align-items:center;gap:10px;margin:8px 0;}
|
|
394
|
+
.swatchrow label{flex:1;font-weight:600;font-size:.92rem;}
|
|
395
|
+
.swatchrow input[type="color"]{flex:0 0 54px;width:54px;}
|
|
396
|
+
|
|
397
|
+
/* ---- Toast ---- */
|
|
398
|
+
.toast{
|
|
399
|
+
position:fixed;bottom:28px;left:50%;transform:translateX(-50%);
|
|
400
|
+
background:var(--accent);color:var(--accent-ink);font-weight:800;padding:12px 20px;border-radius:30px;
|
|
401
|
+
z-index:50;opacity:0;transition:opacity .25s;pointer-events:none;box-shadow:0 4px 16px rgba(0,0,0,.35);max-width:90vw;text-align:center;
|
|
402
|
+
}
|
|
403
|
+
.toast.show{opacity:1;}
|
|
404
|
+
|
|
405
|
+
/* ============================================================
|
|
406
|
+
RESPONSIVE
|
|
407
|
+
============================================================ */
|
|
408
|
+
@media (max-width:860px){
|
|
409
|
+
.pmap{grid-template-columns:1fr;}
|
|
410
|
+
.brow{grid-template-columns:1fr;gap:4px;padding:12px 14px;}
|
|
411
|
+
.barrow{justify-content:flex-start;transform:rotate(90deg);width:1.05rem;}
|
|
412
|
+
dl.gloss{grid-template-columns:1fr;}
|
|
413
|
+
dl.gloss dt{border-bottom:none;padding-bottom:0;}
|
|
414
|
+
.exphead{display:none;}
|
|
415
|
+
.exprow,.exprow.down{grid-template-columns:1fr;gap:7px;padding:14px 0;}
|
|
416
|
+
.expord{order:-1;}
|
|
417
|
+
.distrow{grid-template-columns:112px 1fr 26px;}
|
|
418
|
+
}
|
|
419
|
+
@media (max-width:560px){
|
|
420
|
+
.wrap{padding:16px 13px 70px;}
|
|
421
|
+
header.top{padding:10px 13px 0;}
|
|
422
|
+
h1{font-size:1.3rem;}
|
|
423
|
+
.palette{grid-template-columns:1fr 1fr;}
|
|
424
|
+
.pip{min-width:0;}
|
|
425
|
+
nav.tabs{justify-content:flex-start;}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/* ---- Print: the quick card is the only thing worth paper ---- */
|
|
429
|
+
@media print{
|
|
430
|
+
header.top,nav.tabs,.demo,.chapfoot,.toast,.gmsec{display:none !important;}
|
|
431
|
+
body{background:#fff;color:#000;}
|
|
432
|
+
.qbox{break-inside:avoid;border:1px solid #999;}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/* ---- Pregen cards ---- */
|
|
436
|
+
.pcard h3{color:var(--ink);}
|
|
437
|
+
.pcard .meta{font-size:.85rem;}
|
|
438
|
+
.statrow{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0;}
|
|
439
|
+
.statbox{
|
|
440
|
+
flex:1;min-width:62px;text-align:center;background:var(--surface-2);
|
|
441
|
+
border:1px solid var(--line);border-radius:9px;padding:6px 5px;
|
|
442
|
+
}
|
|
443
|
+
.statbox b{display:block;font-size:1.05rem;color:var(--accent-text);font-variant-numeric:tabular-nums;}
|
|
444
|
+
.statbox span{display:block;font-size:.62rem;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);}
|
|
445
|
+
.refnote{font-weight:400;color:var(--muted);font-size:.85rem;margin-left:6px;}
|
package/src/sw.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* Service worker — offline support for the hosted (GitHub Pages) PWA.
|
|
2
|
+
Network-first for the app page (fresh when online, cached when offline),
|
|
3
|
+
cache-first for static assets. Cache version is stamped at build time so a
|
|
4
|
+
redeploy refreshes clients automatically. */
|
|
5
|
+
const CACHE = "pf2e-primer-__CACHE_VERSION__";
|
|
6
|
+
/* Every cache this app has ever made starts with PREFIX. The sweep below is
|
|
7
|
+
scoped to it on purpose: the three PF2e tools share an origin when they are
|
|
8
|
+
served from one host (GitHub Pages does it, and so does the toolbox), and an
|
|
9
|
+
unscoped sweep would delete the other two apps' offline caches every time
|
|
10
|
+
this one activated. */
|
|
11
|
+
const PREFIX = "pf2e-primer-";
|
|
12
|
+
const SHELL = ["./", "./index.html", "./manifest.webmanifest", "./icon.svg"];
|
|
13
|
+
|
|
14
|
+
self.addEventListener("install", (e) => {
|
|
15
|
+
e.waitUntil(
|
|
16
|
+
caches.open(CACHE)
|
|
17
|
+
.then((c) => c.addAll(SHELL).catch(() => {}))
|
|
18
|
+
.then(() => self.skipWaiting())
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
self.addEventListener("activate", (e) => {
|
|
23
|
+
e.waitUntil(
|
|
24
|
+
caches.keys()
|
|
25
|
+
.then((keys) => Promise.all(keys.filter((k) => k.startsWith(PREFIX) && k !== CACHE)
|
|
26
|
+
.map((k) => caches.delete(k))))
|
|
27
|
+
.then(() => self.clients.claim())
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
self.addEventListener("fetch", (e) => {
|
|
32
|
+
const req = e.request;
|
|
33
|
+
if (req.method !== "GET") return;
|
|
34
|
+
|
|
35
|
+
if (req.mode === "navigate") {
|
|
36
|
+
e.respondWith(
|
|
37
|
+
fetch(req)
|
|
38
|
+
.then((res) => {
|
|
39
|
+
const copy = res.clone();
|
|
40
|
+
caches.open(CACHE).then((c) => c.put("./index.html", copy));
|
|
41
|
+
return res;
|
|
42
|
+
})
|
|
43
|
+
.catch(() => caches.match("./index.html").then((r) => r || caches.match("./")))
|
|
44
|
+
);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
e.respondWith(
|
|
49
|
+
caches.match(req).then((r) =>
|
|
50
|
+
r ||
|
|
51
|
+
fetch(req).then((res) => {
|
|
52
|
+
if (res.ok && new URL(req.url).origin === self.location.origin) {
|
|
53
|
+
const copy = res.clone();
|
|
54
|
+
caches.open(CACHE).then((c) => c.put(req, copy));
|
|
55
|
+
}
|
|
56
|
+
return res;
|
|
57
|
+
}).catch(() => r)
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
|
+
<meta name="theme-color" content="#14161b">
|
|
7
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
8
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
9
|
+
<meta name="apple-mobile-web-app-title" content="PF2e Primer">
|
|
10
|
+
<meta name="description" content="A hands-on introduction to Pathfinder 2e for players who already know tabletop RPGs — the three pillars of play, with live demos of every core rule.">
|
|
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='%233f7d8c' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='5' r='2'/><path d='M12 7v13'/><path d='M8 11h8'/><path d='M5 14a7 7 0 0 0 14 0'/></svg>">
|
|
12
|
+
<!-- Set the theme before first paint (avoids a flash); the engine reconciles on load. -->
|
|
13
|
+
<script>
|
|
14
|
+
(function(){try{
|
|
15
|
+
var s=(JSON.parse(localStorage.getItem("pf2ePrimer.v1"))||{}).settings||{};
|
|
16
|
+
var 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
|
+
if(s.custom&&s.custom.accent) root.style.setProperty("--accent",s.custom.accent);
|
|
20
|
+
}catch(e){}})();
|
|
21
|
+
</script>
|
|
22
|
+
<link rel="manifest" href="manifest.webmanifest">
|
|
23
|
+
<link rel="apple-touch-icon" href="icon.svg">
|
|
24
|
+
<title>PF2e Primer</title>
|
|
25
|
+
<style>
|
|
26
|
+
/*__INJECT_STYLES__*/
|
|
27
|
+
</style>
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
|
|
31
|
+
<header class="top">
|
|
32
|
+
<div class="tophead">
|
|
33
|
+
<h1><span class="apptitle-ic" id="titleIcon"></span> PF2e Primer</h1>
|
|
34
|
+
<div class="headbtns">
|
|
35
|
+
<button class="menubtn" id="gmBtn" onclick="toggleGmMode()" title="GM mode" aria-label="Toggle GM mode">
|
|
36
|
+
<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="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"/></svg>
|
|
37
|
+
GM
|
|
38
|
+
</button>
|
|
39
|
+
<button class="menubtn" onclick="openMenu()" title="Settings" aria-label="Settings">
|
|
40
|
+
<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>
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="sub" id="headSub"></div>
|
|
45
|
+
<div class="progress" id="progressBar"><span style="width:0%"></span></div>
|
|
46
|
+
</header>
|
|
47
|
+
|
|
48
|
+
<nav class="tabs" id="tabs"></nav>
|
|
49
|
+
|
|
50
|
+
<main class="wrap" id="views"></main>
|
|
51
|
+
|
|
52
|
+
<div class="toast" id="toast"></div>
|
|
53
|
+
|
|
54
|
+
<script>
|
|
55
|
+
/*__INJECT_REFERENCE__*/
|
|
56
|
+
</script>
|
|
57
|
+
<script>
|
|
58
|
+
/*__INJECT_CONTENT__*/
|
|
59
|
+
</script>
|
|
60
|
+
<script>
|
|
61
|
+
/*__INJECT_ENGINE__*/
|
|
62
|
+
</script>
|
|
63
|
+
<script>
|
|
64
|
+
(function(){ try { document.getElementById("titleIcon").innerHTML = iconSvg("anchor"); } catch(e){} })();
|
|
65
|
+
</script>
|
|
66
|
+
</body>
|
|
67
|
+
</html>
|
package/tools/build.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
build.py — assemble the single, self-contained dist/index.html.
|
|
4
|
+
|
|
5
|
+
Inlines styles + the Foundry-derived rules text + the primer content +
|
|
6
|
+
the engine into the template, so the result is one file you can open
|
|
7
|
+
offline (file://), text to a player, or host. Run from the project root:
|
|
8
|
+
|
|
9
|
+
python3 tools/build.py
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def read(rel):
|
|
18
|
+
with open(os.path.join(ROOT, rel), encoding="utf-8") as fh:
|
|
19
|
+
return fh.read()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main():
|
|
23
|
+
html = read("src/template.html")
|
|
24
|
+
parts = {
|
|
25
|
+
"/*__INJECT_STYLES__*/": read("src/styles.css"),
|
|
26
|
+
"/*__INJECT_REFERENCE__*/": read("data/reference.generated.js"),
|
|
27
|
+
"/*__INJECT_CONTENT__*/": read("src/content.js"),
|
|
28
|
+
"/*__INJECT_ENGINE__*/": read("src/engine.js"),
|
|
29
|
+
}
|
|
30
|
+
for marker, content in parts.items():
|
|
31
|
+
if marker not in html:
|
|
32
|
+
raise SystemExit(f"marker not found in template: {marker}")
|
|
33
|
+
# str.replace (not regex) so JS/CSS content is inserted verbatim
|
|
34
|
+
html = html.replace(marker, content)
|
|
35
|
+
|
|
36
|
+
out_dir = os.path.join(ROOT, "dist")
|
|
37
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
38
|
+
out_path = os.path.join(out_dir, "index.html")
|
|
39
|
+
with open(out_path, "w", encoding="utf-8") as fh:
|
|
40
|
+
fh.write(html)
|
|
41
|
+
|
|
42
|
+
# Static PWA files (for the hosted GitHub Pages version). Harmlessly absent
|
|
43
|
+
# when index.html is opened as a downloaded standalone file.
|
|
44
|
+
cache_ver = time.strftime("%Y%m%d%H%M%S")
|
|
45
|
+
for name in ("icon.svg", "manifest.webmanifest", "sw.js"):
|
|
46
|
+
src = os.path.join(ROOT, "src", name)
|
|
47
|
+
if not os.path.exists(src):
|
|
48
|
+
continue
|
|
49
|
+
content = read("src/" + name)
|
|
50
|
+
if name == "sw.js":
|
|
51
|
+
content = content.replace("__CACHE_VERSION__", cache_ver)
|
|
52
|
+
with open(os.path.join(out_dir, name), "w", encoding="utf-8") as fh:
|
|
53
|
+
fh.write(content)
|
|
54
|
+
|
|
55
|
+
kb = os.path.getsize(out_path) / 1024
|
|
56
|
+
print(f"built {out_path} ({kb:.0f} KB) + PWA files (cache {cache_ver})")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
main()
|