pf2e-sage-stats 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -0
- package/dist/app.js +409 -0
- package/dist/index.js +199 -0
- package/eslint.config.mjs +11 -0
- package/jest.config.ts +11 -0
- package/package.json +40 -0
- package/src/app.ts +623 -0
- package/src/index.ts +198 -0
- package/src/types.d.ts +5 -0
- package/test/app.test.ts +67 -0
- package/tsconfig.json +14 -0
package/src/app.ts
ADDED
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import zod from 'zod';
|
|
2
|
+
import { TSV } from 'tsv';
|
|
3
|
+
import { capitalize, entries, startCase, toLower, kebabCase } from 'lodash';
|
|
4
|
+
import dedent from 'dedent-js';
|
|
5
|
+
import abbreviate from 'abbreviate';
|
|
6
|
+
import pluralize from 'pluralize';
|
|
7
|
+
|
|
8
|
+
const schema = zod.object({
|
|
9
|
+
name: zod.string(),
|
|
10
|
+
alias: zod.string().optional(),
|
|
11
|
+
level: zod.coerce.number().default(0),
|
|
12
|
+
maxhp: zod.coerce.number().default(0),
|
|
13
|
+
attributes: zod.object({
|
|
14
|
+
strength: zod.coerce.number().default(0),
|
|
15
|
+
dexterity: zod.coerce.number().default(0),
|
|
16
|
+
constitution: zod.coerce.number().default(0),
|
|
17
|
+
intelligence: zod.coerce.number().default(0),
|
|
18
|
+
wisdom: zod.coerce.number().default(0),
|
|
19
|
+
charisma: zod.coerce.number().default(0),
|
|
20
|
+
}).default({}),
|
|
21
|
+
saves: zod.object({
|
|
22
|
+
fortitude: zod.coerce.number().default(0),
|
|
23
|
+
reflex: zod.coerce.number().default(0),
|
|
24
|
+
will: zod.coerce.number().default(0),
|
|
25
|
+
}).default({}),
|
|
26
|
+
ac: zod.coerce.number().default(10),
|
|
27
|
+
perception: zod.coerce.number().default(0),
|
|
28
|
+
skills: zod.object({
|
|
29
|
+
acrobatics: zod.coerce.number().optional(),
|
|
30
|
+
arcana: zod.coerce.number().optional(),
|
|
31
|
+
athletics: zod.coerce.number().optional(),
|
|
32
|
+
crafting: zod.coerce.number().optional(),
|
|
33
|
+
deception: zod.coerce.number().optional(),
|
|
34
|
+
diplomacy: zod.coerce.number().optional(),
|
|
35
|
+
intimidation: zod.coerce.number().optional(),
|
|
36
|
+
medicine: zod.coerce.number().optional(),
|
|
37
|
+
nature: zod.coerce.number().optional(),
|
|
38
|
+
occultism: zod.coerce.number().optional(),
|
|
39
|
+
perfomance: zod.coerce.number().optional(),
|
|
40
|
+
religion: zod.coerce.number().optional(),
|
|
41
|
+
society: zod.coerce.number().optional(),
|
|
42
|
+
stealth: zod.coerce.number().optional(),
|
|
43
|
+
survival: zod.coerce.number().optional(),
|
|
44
|
+
thievery: zod.coerce.number().optional(),
|
|
45
|
+
}).default({}),
|
|
46
|
+
lores: zod.record(zod.string(), zod.object({
|
|
47
|
+
mod: zod.coerce.number().default(0),
|
|
48
|
+
name: zod.string(),
|
|
49
|
+
})).default({}),
|
|
50
|
+
melee: zod.record(zod.string(), zod.object({
|
|
51
|
+
mod: zod.coerce.number().default(0),
|
|
52
|
+
desc: zod.string(),
|
|
53
|
+
damage: zod.string(),
|
|
54
|
+
})).default({}),
|
|
55
|
+
ranged: zod.record(zod.string(), zod.object({
|
|
56
|
+
mod: zod.coerce.number().default(0),
|
|
57
|
+
desc: zod.string(),
|
|
58
|
+
damage: zod.string(),
|
|
59
|
+
})).default({}),
|
|
60
|
+
spells: zod.object({
|
|
61
|
+
attack: zod.coerce.number().optional(),
|
|
62
|
+
dc: zod.coerce.number().optional(),
|
|
63
|
+
}).default({}),
|
|
64
|
+
extra: zod.record(zod.string(), zod.coerce.number()).default({}),
|
|
65
|
+
extraDCs: zod.record(zod.string(), zod.coerce.number()).default({}),
|
|
66
|
+
extraDice: zod.record(zod.string(), zod.coerce.string()).default({}),
|
|
67
|
+
improvisation: zod.boolean().default(false),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
export type Schema = zod.infer<typeof schema>;
|
|
71
|
+
|
|
72
|
+
export const stub: Required<Schema> = {
|
|
73
|
+
name: 'name',
|
|
74
|
+
alias: 'nm',
|
|
75
|
+
level: 0,
|
|
76
|
+
maxhp: 0,
|
|
77
|
+
attributes: {
|
|
78
|
+
strength: 0,
|
|
79
|
+
dexterity: 0,
|
|
80
|
+
constitution: 0,
|
|
81
|
+
intelligence: 0,
|
|
82
|
+
wisdom: 0,
|
|
83
|
+
charisma: 0,
|
|
84
|
+
},
|
|
85
|
+
saves: {
|
|
86
|
+
fortitude: 0,
|
|
87
|
+
reflex: 0,
|
|
88
|
+
will: 0,
|
|
89
|
+
},
|
|
90
|
+
ac: 10,
|
|
91
|
+
perception: 0,
|
|
92
|
+
skills: {
|
|
93
|
+
acrobatics: 0,
|
|
94
|
+
arcana: 0,
|
|
95
|
+
athletics: 0,
|
|
96
|
+
crafting: 0,
|
|
97
|
+
deception: 0,
|
|
98
|
+
diplomacy: 0,
|
|
99
|
+
intimidation: 0,
|
|
100
|
+
medicine: 0,
|
|
101
|
+
nature: 0,
|
|
102
|
+
occultism: 0,
|
|
103
|
+
perfomance: 0,
|
|
104
|
+
religion: 0,
|
|
105
|
+
society: 0,
|
|
106
|
+
stealth: 0,
|
|
107
|
+
survival: 0,
|
|
108
|
+
thievery: 0,
|
|
109
|
+
},
|
|
110
|
+
lores: {},
|
|
111
|
+
melee: {
|
|
112
|
+
fist: { mod: 7, desc: 'fist (agile, finesse)', damage: '1d4 bludgeoning' }
|
|
113
|
+
},
|
|
114
|
+
ranged: {},
|
|
115
|
+
spells: {},
|
|
116
|
+
extra: {},
|
|
117
|
+
extraDCs: {},
|
|
118
|
+
extraDice: {},
|
|
119
|
+
improvisation: false,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const formatMap: Record<string, string> = {
|
|
123
|
+
xxs: 'xxs',
|
|
124
|
+
xs: 'xs',
|
|
125
|
+
s: 's',
|
|
126
|
+
m: 'm',
|
|
127
|
+
l: 'l',
|
|
128
|
+
xl: 'xl',
|
|
129
|
+
xxl: 'xxl',
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const prediceateMap: Record<string, string> = {
|
|
133
|
+
...([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map((v): [string, string] => (
|
|
134
|
+
[`${v}`, `d20 >= ${v} flat;`]
|
|
135
|
+
)).reduce<Record<string, string>>((p, [k, v]) => { p[k] = v; return p }, {})),
|
|
136
|
+
c: 'd20 >= 5 flat;',
|
|
137
|
+
concealed: 'd20 >= 5 flat;',
|
|
138
|
+
h: 'd20 >= 11 flat;',
|
|
139
|
+
hidden: 'd20 >= 11 flat;',
|
|
140
|
+
none: '',
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const flatMap = Object.entries(prediceateMap).flatMap(([k, v]) => Object.entries(formatMap).map(([fk, fv]): [string, string] => (
|
|
144
|
+
[`${fk}_${k}`, `${fv} ${v}`]
|
|
145
|
+
))).reduce<Record<string, string>>((p, [k, v]) => { p[k] = v; return p }, {});
|
|
146
|
+
|
|
147
|
+
export const valueNameMap: (tag: string | null) => Record<string, string> = (tag: string | null) => ({
|
|
148
|
+
...([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map((v): [string, string] => (
|
|
149
|
+
[`+${v}`, tag ? ` ⟮+${v} ${tag}⟯` : ` ⟮+${v}⟯`]
|
|
150
|
+
)).reduce<Record<string, string>>((p, [k, v]) => { p[k] = v; return p }, {})),
|
|
151
|
+
...([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map((v): [string, string] => (
|
|
152
|
+
[`${v}`, tag ? ` ⟮+${v} ${tag}⟯` : ` ⟮+${v}⟯`]
|
|
153
|
+
)).reduce<Record<string, string>>((p, [k, v]) => { p[k] = v; return p }, {})),
|
|
154
|
+
...([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map((v): [string, string] => (
|
|
155
|
+
[`-${v}`, tag ? ` ⟮-${v} ${tag}⟯` : ` ⟮-${v}⟯`]
|
|
156
|
+
)).reduce<Record<string, string>>((p, [k, v]) => { p[k] = v; return p }, {})),
|
|
157
|
+
'+0': '',
|
|
158
|
+
'0': '',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
export const valueMap: () => Record<string, string> = () => ({
|
|
162
|
+
...([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map((v): [string, string] => (
|
|
163
|
+
[`+${v}`, `+${v}`]
|
|
164
|
+
)).reduce<Record<string, string>>((p, [k, v]) => { p[k] = v; return p }, {})),
|
|
165
|
+
...([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map((v): [string, string] => (
|
|
166
|
+
[`${v}`, `+${v}`]
|
|
167
|
+
)).reduce<Record<string, string>>((p, [k, v]) => { p[k] = v; return p }, {})),
|
|
168
|
+
...([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map((v): [string, string] => (
|
|
169
|
+
[`-${v}`, `-${v}`]
|
|
170
|
+
)).reduce<Record<string, string>>((p, [k, v]) => { p[k] = v; return p }, {})),
|
|
171
|
+
'+0': '+0',
|
|
172
|
+
'0': '+0',
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
export const diceNameMap: () => Record<string, string> = () => ({
|
|
176
|
+
...([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map((v): [string, string] => (
|
|
177
|
+
[`(${v})`, ` ⟮take ${v}⟯`]
|
|
178
|
+
)).reduce<Record<string, string>>((p, [k, v]) => { p[k] = v; return p }, {})),
|
|
179
|
+
'1': '',
|
|
180
|
+
'+1': '',
|
|
181
|
+
'-1': '',
|
|
182
|
+
'2': ' ⟮fortune⟯',
|
|
183
|
+
'+2': ' ⟮fortune⟯',
|
|
184
|
+
'-2': ' ⟮misfortune⟯',
|
|
185
|
+
'a': ' ⟮fortune⟯',
|
|
186
|
+
'adv': ' ⟮fortune⟯',
|
|
187
|
+
'advantage': ' ⟮fortune⟯',
|
|
188
|
+
'd': ' ⟮misfortune⟯',
|
|
189
|
+
'dis': ' ⟮misfortune⟯',
|
|
190
|
+
'disadvantage': ' ⟮misfortune⟯',
|
|
191
|
+
'f': ' ⟮fortune⟯',
|
|
192
|
+
'for': ' ⟮fortune⟯',
|
|
193
|
+
'fortune': ' ⟮fortune⟯',
|
|
194
|
+
'm': ' ⟮misfortune⟯',
|
|
195
|
+
'mis': ' ⟮misfortune⟯',
|
|
196
|
+
'misfortune': ' ⟮misfortune⟯',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
export const diceMap: () => Record<string, string> = () => ({
|
|
200
|
+
...([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map((v): [string, string] => (
|
|
201
|
+
[`(${v})`, `(${v})`]
|
|
202
|
+
)).reduce<Record<string, string>>((p, [k, v]) => { p[k] = v; return p }, {})),
|
|
203
|
+
'1': '1',
|
|
204
|
+
'+1': '1',
|
|
205
|
+
'-1': '1',
|
|
206
|
+
'2': '+2',
|
|
207
|
+
'+2': '+2',
|
|
208
|
+
'-2': '-2',
|
|
209
|
+
'a': '+2',
|
|
210
|
+
'adv': '+2',
|
|
211
|
+
'advantage': '+2',
|
|
212
|
+
'd': '-2',
|
|
213
|
+
'dis': '-2',
|
|
214
|
+
'disadvantage': '-2',
|
|
215
|
+
'f': '+2',
|
|
216
|
+
'for': '+2',
|
|
217
|
+
'fortune': '+2',
|
|
218
|
+
'm': '-2',
|
|
219
|
+
'mis': '-2',
|
|
220
|
+
'misfortune': '-2',
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
export const adjustmentMap: () => Record<string, string> = () => ({
|
|
224
|
+
'incredibly-easy': ' ⟮Incredibly Easy⟯',
|
|
225
|
+
'very-easy': ' ⟮Very Easy⟯',
|
|
226
|
+
'easy': ' ⟮Easy⟯',
|
|
227
|
+
'default': '',
|
|
228
|
+
'hard': ' ⟮Hard⟯',
|
|
229
|
+
'very-hard': ' ⟮Very Easy⟯',
|
|
230
|
+
'incredibly-hard': ' ⟮Incredibly Hard⟯',
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
export const fromatMap = (name: string, map: Record<string, string>) => {
|
|
234
|
+
return TSV.stringify([{
|
|
235
|
+
name,
|
|
236
|
+
...map,
|
|
237
|
+
}]);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const dcByLevel = [13, 14, 15, 16, 18, 19, 20, 22, 23, 24, 26, 27, 28, 30, 31, 32, 34, 35, 36, 38, 39, 40, 42, 44, 46, 48, 50]
|
|
241
|
+
|
|
242
|
+
export const formatJSON = (stats: Schema): string => JSON.stringify(stats, undefined, 2);
|
|
243
|
+
export const parseJSON = (json: string): Schema => schema.parse(JSON.parse(json));
|
|
244
|
+
|
|
245
|
+
const locateName = (statblock: string): { name: string; level: number } | null => {
|
|
246
|
+
const regex = /^([^()]+?)(\s+\(\d+\))?\s+\S+\s+(-?\d+)$/m;
|
|
247
|
+
|
|
248
|
+
const match = statblock.match(regex);
|
|
249
|
+
|
|
250
|
+
if (match && match[1] && match[3]) {
|
|
251
|
+
return { name: pluralize(startCase(toLower(match[1].replace(/\s+/, ' ').trim())), 1), level: parseInt(match[3]) };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const locateInt = <T extends string>(name: T, statblock: string, section: string | null = null, signed: boolean = true, alias?: string): { [k in T]: number } | null => {
|
|
258
|
+
const match = statblock.match(new RegExp(`${section ? `(${section}|,)\\s+` : '()'}${alias ?? capitalize(name)}\\s+(${signed ? '[+-]' : '-?'}\\d+)`))
|
|
259
|
+
|
|
260
|
+
if (match && match[2]) {
|
|
261
|
+
return { [name]: parseInt(match[2]) } as { [k in T]: number };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const locateInts = (statblock: string, section: string, alias: string, signed: boolean = true): Record<string, { mod: number; name: string; }> => {
|
|
268
|
+
const regex = new RegExp(`(${section}|,)([^,]*?)${alias}\\s+(${signed ? '[+-]' : '-?'}\\d+)`, 'g');
|
|
269
|
+
|
|
270
|
+
let matches: RegExpExecArray | null = null;
|
|
271
|
+
const output: [string, string, number][] = [];
|
|
272
|
+
// eslint-disable-next-line no-cond-assign
|
|
273
|
+
while (matches = regex.exec(statblock)) {
|
|
274
|
+
const [,, name, mod] = matches;
|
|
275
|
+
|
|
276
|
+
if (name && mod) {
|
|
277
|
+
output.push([
|
|
278
|
+
name.toLowerCase().trim().replace(/\s+/g, '-'),
|
|
279
|
+
name.trim(),
|
|
280
|
+
parseInt(mod),
|
|
281
|
+
]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return output.reduce<Record<string, { mod: number; name: string; }>>((p, [key, name, value]) => { p[key] = { mod: value, name: name }; return p; }, {});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const locateIntsAfter = (statblock: string, alias: string, signed: boolean = true): Record<string, { mod: number; name: string; }> => {
|
|
289
|
+
const regex = new RegExp(`${alias}([\\s\\S]*?)(${signed ? '[+-]' : '-?'}\\d+)`, 'g');
|
|
290
|
+
|
|
291
|
+
let matches: RegExpExecArray | null = null;
|
|
292
|
+
const output: [string, string, number][] = [];
|
|
293
|
+
// eslint-disable-next-line no-cond-assign
|
|
294
|
+
while (matches = regex.exec(statblock)) {
|
|
295
|
+
const [,name, mod] = matches;
|
|
296
|
+
|
|
297
|
+
if (name && mod) {
|
|
298
|
+
output.push([
|
|
299
|
+
name.toLowerCase().trim().replace(/\s+/g, '-'),
|
|
300
|
+
name.trim(),
|
|
301
|
+
parseInt(mod),
|
|
302
|
+
]);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return output.reduce<Record<string, { mod: number; name: string; }>>((p, [key, name, value]) => { p[key] = { mod: value, name: name }; return p; }, {});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const locateStrikes = (statblock: string, alias: string): Record<string, Schema['melee'][string]> => {
|
|
310
|
+
const regex = new RegExp(`${alias}(\\s+\\[.+\\])?\\s+(.+?)\\s+(\\(x\\d+\\)\\s+)?([+-]\\d+)\\s*([\\S\\s]*?),\\s+(Damage|Effect)\\s+(\\S+\\s+\\S+.*?)(?=$|\\s+Melee|\\s+Ranged)`, 'gm');
|
|
311
|
+
|
|
312
|
+
let matches: RegExpExecArray | null = null;
|
|
313
|
+
const output: [string, number, string, string][] = [];
|
|
314
|
+
// eslint-disable-next-line no-cond-assign
|
|
315
|
+
while (matches = regex.exec(statblock)) {
|
|
316
|
+
const [, , name, , attack, traits, , dice] = matches;
|
|
317
|
+
|
|
318
|
+
if (name && attack && dice) {
|
|
319
|
+
const _name = name.toLowerCase()
|
|
320
|
+
.replace(/\s+/g, ' ')
|
|
321
|
+
.replace(/\+\d/g, '')
|
|
322
|
+
.replace(/Weapon\s+Striking(\s+\((Greater|Major)\))?/gi, '')
|
|
323
|
+
.replace(/\(Agile\)/gi, '')
|
|
324
|
+
.replace(/\(\+\)/gi, '')
|
|
325
|
+
.trim();
|
|
326
|
+
|
|
327
|
+
const bps = /(?<=[^\w']|^)[BbPpSs](?=[^\w]|$)/g;
|
|
328
|
+
|
|
329
|
+
let _traits = traits && traits.toLowerCase()
|
|
330
|
+
.replace(/\s+/g, ' ')
|
|
331
|
+
.replace(bps, (m) => m.toUpperCase())
|
|
332
|
+
.replace(/1d/, 'd')
|
|
333
|
+
.trim();
|
|
334
|
+
|
|
335
|
+
const _dice = dice
|
|
336
|
+
.replace(/\s+/g, ' ')
|
|
337
|
+
.replace(bps, (m) => ({ b: 'bludgeoning', p: 'piercing', s: 'slashing' }[m.toLowerCase()] ?? m))
|
|
338
|
+
.trim();
|
|
339
|
+
|
|
340
|
+
const twoHand = _traits.match(/(two-hand|two-handed)\s+(d\d+)/);
|
|
341
|
+
|
|
342
|
+
if (twoHand && twoHand[2]) {
|
|
343
|
+
_traits = _traits.replace(/(two-hand|two-handed)\s+d\d+/g, 'two-hand');
|
|
344
|
+
|
|
345
|
+
const dice2h = _dice.replace(/(?<=\d)d\d+/, twoHand[2]);
|
|
346
|
+
|
|
347
|
+
output.push([
|
|
348
|
+
_name.replace(/\s+/g, '-') + '-2h',
|
|
349
|
+
parseInt(attack),
|
|
350
|
+
`2h ${_traits ? `${_name} ${_traits}` : _name}`,
|
|
351
|
+
dice2h,
|
|
352
|
+
], [
|
|
353
|
+
_name.replace(/\s+/g, '-') + '-1h',
|
|
354
|
+
parseInt(attack),
|
|
355
|
+
`1h ${_traits ? `${_name} ${_traits}` : _name}`,
|
|
356
|
+
_dice,
|
|
357
|
+
]);
|
|
358
|
+
} else {
|
|
359
|
+
output.push([
|
|
360
|
+
_name.replace(/\s+/g, '-'),
|
|
361
|
+
parseInt(attack),
|
|
362
|
+
_traits ? `${_name} ${_traits}` : _name,
|
|
363
|
+
_dice,
|
|
364
|
+
]);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return output.reduce<Record<string, Schema['melee'][string]>>((p, [key, m, t, d]) => { p[key] = {
|
|
370
|
+
mod: m,
|
|
371
|
+
desc: t,
|
|
372
|
+
damage: d,
|
|
373
|
+
}; return p; }, {});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const locateSpells = (statblock: string): Schema['spells'] | null => {
|
|
377
|
+
const match = statblock.match(new RegExp(/Spells\s+DC\s+(\d+)(,\s+[Aa]ttack\s+([+-]\d+))?/));
|
|
378
|
+
|
|
379
|
+
if (match && match[1]) {
|
|
380
|
+
return {
|
|
381
|
+
dc: parseInt(match[1]),
|
|
382
|
+
...(match[3] ? { attack: parseInt(match[3]) } : null)
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export const parseStatblock = (name: string | null, _statblock: string, alias: string | null): Schema => {
|
|
390
|
+
const defaults = schema.parse({ name: name ?? 'Unknown' });
|
|
391
|
+
|
|
392
|
+
const statblock = _statblock
|
|
393
|
+
.replace(/^Items.*$/m, '')
|
|
394
|
+
.replace(/–/g, '-') + '\n';
|
|
395
|
+
|
|
396
|
+
const improvisation = !!statblock.match(/Untrained\s+Improvisation/);
|
|
397
|
+
|
|
398
|
+
const basic = locateName(statblock);
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
...defaults,
|
|
402
|
+
...basic,
|
|
403
|
+
...(name && { name: name }),
|
|
404
|
+
...{ alias: alias ?? abbreviate(name ?? basic?.name ?? defaults.name, { length: 3 }).toLowerCase() },
|
|
405
|
+
...locateInt('perception', statblock),
|
|
406
|
+
...locateInt('ac', statblock, null, false, 'AC'),
|
|
407
|
+
...locateInt('maxhp', statblock, null, false, 'HP'),
|
|
408
|
+
attributes: {
|
|
409
|
+
...defaults.attributes,
|
|
410
|
+
...locateInt('strength', statblock, null, true, 'Str'),
|
|
411
|
+
...locateInt('dexterity', statblock, null, true, 'Dex'),
|
|
412
|
+
...locateInt('constitution', statblock, null, true, 'Con'),
|
|
413
|
+
...locateInt('intelligence', statblock, null, true, 'Int'),
|
|
414
|
+
...locateInt('wisdom', statblock, null, true, 'Wis'),
|
|
415
|
+
...locateInt('charisma', statblock, null, true, 'Cha'),
|
|
416
|
+
},
|
|
417
|
+
saves: {
|
|
418
|
+
...defaults.saves,
|
|
419
|
+
...locateInt('fortitude', statblock, null, true, 'Fortitude'),
|
|
420
|
+
...locateInt('reflex', statblock, null, true, 'Reflex'),
|
|
421
|
+
...locateInt('fortitude', statblock, null, true, 'Fort'),
|
|
422
|
+
...locateInt('reflex', statblock, null, true, 'Ref'),
|
|
423
|
+
...locateInt('will', statblock, null, true, 'Will'),
|
|
424
|
+
},
|
|
425
|
+
skills: {
|
|
426
|
+
...defaults.skills,
|
|
427
|
+
...locateInt('acrobatics', statblock, 'Skills'),
|
|
428
|
+
...locateInt('arcana', statblock, 'Skills'),
|
|
429
|
+
...locateInt('athletics', statblock, 'Skills'),
|
|
430
|
+
...locateInt('crafting', statblock, 'Skills'),
|
|
431
|
+
...locateInt('deception', statblock, 'Skills'),
|
|
432
|
+
...locateInt('diplomacy', statblock, 'Skills'),
|
|
433
|
+
...locateInt('intimidation', statblock, 'Skills'),
|
|
434
|
+
...locateInt('medicine', statblock, 'Skills'),
|
|
435
|
+
...locateInt('nature', statblock, 'Skills'),
|
|
436
|
+
...locateInt('occultism', statblock, 'Skills'),
|
|
437
|
+
...locateInt('perfomance', statblock, 'Skills'),
|
|
438
|
+
...locateInt('religion', statblock, 'Skills'),
|
|
439
|
+
...locateInt('society', statblock, 'Skills'),
|
|
440
|
+
...locateInt('stealth', statblock, 'Skills'),
|
|
441
|
+
...locateInt('survival', statblock, 'Skills'),
|
|
442
|
+
...locateInt('thievery', statblock, 'Skills'),
|
|
443
|
+
},
|
|
444
|
+
lores: {
|
|
445
|
+
...locateInts(statblock, 'Skills', 'Lore'),
|
|
446
|
+
...locateIntsAfter(statblock, 'Lore:')
|
|
447
|
+
},
|
|
448
|
+
melee: {
|
|
449
|
+
...locateStrikes(statblock, 'Melee'),
|
|
450
|
+
},
|
|
451
|
+
ranged: {
|
|
452
|
+
...locateStrikes(statblock, 'Ranged'),
|
|
453
|
+
},
|
|
454
|
+
spells: {
|
|
455
|
+
...locateSpells(statblock),
|
|
456
|
+
},
|
|
457
|
+
extra: {},
|
|
458
|
+
extraDCs: {
|
|
459
|
+
...locateInt('stealth', statblock, null, false, 'Stealth DC'),
|
|
460
|
+
},
|
|
461
|
+
extraDice: {},
|
|
462
|
+
improvisation,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const dc2DC = (secret: boolean) => ([key, value]: [string | number, number]): [string, string] => [`dc.${key}`, secret ? `||${value}||` : `${value}`];
|
|
467
|
+
const mod2DC = (secret: boolean) => ([key, value]: [string | number, number]): [string, string] => [`dc.${key}`, secret ? `||${10 + value}||` : `${10 + value}`];
|
|
468
|
+
const toLore = ([key, value]: [string | number, { mod: number; name: string; }]): [string, number| string][] => [[`lore.${key}`, value.mod], [`lore.${key}.name`, value.name]];
|
|
469
|
+
const toMelee = ([key, value]: [string, Schema['melee'][string]]): [string, number | string][] => [
|
|
470
|
+
[`melee.${key}`, value.mod],
|
|
471
|
+
[`melee.${key}.desc`, value.desc],
|
|
472
|
+
[`melee.${key}.damage`, value.damage],
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
const toRanged = ([key, value]: [string, Schema['ranged'][string]]): [string, number | string][] => [
|
|
476
|
+
[`ranged.${key}`, value.mod],
|
|
477
|
+
[`ranged.${key}.desc`, value.desc],
|
|
478
|
+
[`ranged.${key}.damage`, value.damage],
|
|
479
|
+
];
|
|
480
|
+
|
|
481
|
+
const recallDCs = (level: number, secret: boolean): [string, string][] => ((value) => [
|
|
482
|
+
['dc.recall.incredibly-easy', secret ? `||${value - 10}||` : `${value - 10}`],
|
|
483
|
+
['dc.recall.very-easy', secret ? `||${value - 5}||` : `${value - 5}`],
|
|
484
|
+
['dc.recall.easy', secret ? `||${value - 2}||` : `${value - 2}`],
|
|
485
|
+
['dc.recall.default', secret ? `||${value}||` : `${value}`],
|
|
486
|
+
['dc.recall', secret ? `||${value}||` : `${value}`],
|
|
487
|
+
['dc.recall.hard', secret ? `||${value + 2}||` : `${value + 2}`],
|
|
488
|
+
['dc.recall.very-hard', secret ? `||${value + 5}||` : `${value + 5}`],
|
|
489
|
+
['dc.recall.incredibly-hard', secret ? `||${value + 10}||` : `${value + 10}`],
|
|
490
|
+
])(dcByLevel[Math.max(Math.min(level + 1, dcByLevel.length - 1), 0)]);
|
|
491
|
+
|
|
492
|
+
export const flatten = (stats: Schema, secretDC: boolean = false, defaultSkills: boolean = false, recallDC: boolean = false, desc: boolean): Record<string, string | number> => {
|
|
493
|
+
const untrained = stats.improvisation ? (stats.level >= 7 ? stats.level : stats.level >= 5 ? stats.level - 1 : stats.level - 2) : 0;
|
|
494
|
+
const skills = defaultSkills ? {
|
|
495
|
+
acrobatics: untrained + stats.attributes.dexterity,
|
|
496
|
+
arcana: untrained + stats.attributes.intelligence,
|
|
497
|
+
athletics: untrained + stats.attributes.strength,
|
|
498
|
+
crafting: untrained + stats.attributes.intelligence,
|
|
499
|
+
deception: untrained + stats.attributes.charisma,
|
|
500
|
+
diplomacy: untrained + stats.attributes.charisma,
|
|
501
|
+
intimidation: untrained + stats.attributes.charisma,
|
|
502
|
+
medicine: untrained + stats.attributes.wisdom,
|
|
503
|
+
nature: untrained + stats.attributes.wisdom,
|
|
504
|
+
occultism: untrained + stats.attributes.intelligence,
|
|
505
|
+
perfomance: untrained + stats.attributes.charisma,
|
|
506
|
+
religion: untrained + stats.attributes.wisdom,
|
|
507
|
+
society: untrained + stats.attributes.intelligence,
|
|
508
|
+
stealth: untrained + stats.attributes.dexterity,
|
|
509
|
+
survival: untrained + stats.attributes.wisdom,
|
|
510
|
+
thievery: untrained + stats.attributes.dexterity,
|
|
511
|
+
...stats.skills,
|
|
512
|
+
} : stats.skills;
|
|
513
|
+
|
|
514
|
+
let melee = entries(stats.melee);
|
|
515
|
+
if (melee.length > 0) {
|
|
516
|
+
melee = [
|
|
517
|
+
['default', melee[0][1]],
|
|
518
|
+
...melee,
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
let ranged = entries(stats.ranged);
|
|
523
|
+
if (ranged.length > 0) {
|
|
524
|
+
ranged = [
|
|
525
|
+
['default', ranged[0][1]],
|
|
526
|
+
...ranged,
|
|
527
|
+
];
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return [
|
|
531
|
+
['name', stats.name],
|
|
532
|
+
...(stats.alias ? [['alias', stats.alias]] : []),
|
|
533
|
+
['gamesystem', 'pf2e'],
|
|
534
|
+
['level', stats.level],
|
|
535
|
+
['lvl', stats.level],
|
|
536
|
+
['maxhp', stats.maxhp],
|
|
537
|
+
...entries(stats.attributes),
|
|
538
|
+
...entries(stats.attributes).map(([key, value]) => [key.substring(0,3), value]),
|
|
539
|
+
['ac', secretDC ? `||${stats.ac}||` : stats.ac],
|
|
540
|
+
...entries(stats.saves),
|
|
541
|
+
['fort', stats.saves.fortitude],
|
|
542
|
+
['ref', stats.saves.reflex],
|
|
543
|
+
['perception', stats.perception],
|
|
544
|
+
...entries(skills),
|
|
545
|
+
...entries(stats.lores).flatMap(toLore),
|
|
546
|
+
...(desc ? [
|
|
547
|
+
...entries(stats.saves).map(([key]) => [`${key}.desc`, `${capitalize(key)} Save`]),
|
|
548
|
+
['fort.desc', 'Fortitude Save'],
|
|
549
|
+
['ref.desc', 'Reflex Save'],
|
|
550
|
+
['perception.desc', 'Perception Check'],
|
|
551
|
+
...entries(skills).map(([key]) => [`${key}.desc`, `${capitalize(key)} Check`]),
|
|
552
|
+
...entries(stats.lores).map(([key, value]) => [`lore.${key}.desc`, `${value.name} Lore Check`]),
|
|
553
|
+
...(stats.spells.attack ? [['spells.desc', 'Spell Attack']] : []),
|
|
554
|
+
]: []),
|
|
555
|
+
...melee.flatMap(toMelee),
|
|
556
|
+
...ranged.flatMap(toRanged),
|
|
557
|
+
...entries(stats.extra),
|
|
558
|
+
...(stats.spells.attack ? [['spells', stats.spells.attack]] : []),
|
|
559
|
+
...(stats.spells.dc ? [dc2DC(secretDC)(['spells', stats.spells.dc])] : []),
|
|
560
|
+
...entries(stats.extraDCs).map(dc2DC(secretDC)),
|
|
561
|
+
...entries(stats.extraDice),
|
|
562
|
+
...(secretDC ? [
|
|
563
|
+
...entries(stats.saves).map(mod2DC(true)),
|
|
564
|
+
mod2DC(true)(['perception', stats.perception]),
|
|
565
|
+
mod2DC(true)(['fort', stats.saves.fortitude]),
|
|
566
|
+
mod2DC(true)(['ref', stats.saves.reflex]),
|
|
567
|
+
...entries(skills).map(mod2DC(true)),
|
|
568
|
+
...entries(stats.extra).map(mod2DC(true)),
|
|
569
|
+
] : []),
|
|
570
|
+
...(recallDC ? [
|
|
571
|
+
...recallDCs(stats.level, secretDC),
|
|
572
|
+
] : []),
|
|
573
|
+
].reduce<Record<string | number, string | number>>((p, [key, value]) => { p[key] = value; return p }, {});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export const formatTSV = (stats: Schema, secretDC: boolean = false, defaultSkills: boolean = false, recallDC: boolean = false, desc: boolean = false): string => (
|
|
577
|
+
TSV.stringify([flatten(stats, secretDC, defaultSkills, recallDC, desc)])
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
export const formatCommand = (stats: Schema, secretDC: boolean = false, defaultSkills: boolean = false, recallDC: boolean = false, desc: boolean = false): string => (
|
|
581
|
+
Object.entries(flatten(stats, secretDC, defaultSkills, recallDC, desc)).map(([k, v]) => `${k}="${v}"`).join(' ')
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
export const formatTable = (table: number, season: number, scenario: number, _name: string, gm: string, players: string) => {
|
|
585
|
+
const name = _name.replace(/[\s-_]/g, ' ').replace(/[^a-zA-Z0-9 ]/g, '');
|
|
586
|
+
|
|
587
|
+
const gameName = `PF2 T${table} S${season} ${String(scenario).padStart(2, '0')} ${name}`;
|
|
588
|
+
const tableName = `pf2-t${table}-s${season}${String(scenario).padStart(2, '0')}-${kebabCase(name)}`;
|
|
589
|
+
|
|
590
|
+
return dedent`
|
|
591
|
+
\`\`\`
|
|
592
|
+
@TableBot create "${gameName}" --gm ${gm} --players ${players} --table-name ${tableName} --ooc-table-name ooc-${tableName} --category Game Tables
|
|
593
|
+
\`\`\`
|
|
594
|
+
\`\`\`
|
|
595
|
+
sage! game create name="${gameName}" gameSystem="pf2e" ic=" #${tableName} " ooc=" #ooc-${tableName} " gms=" ${gm} " players=" @${gameName} " dialogPost="post" diceSecret="gm" diceCrit="timestwo" diceOutput=M gmCharName="Хронист" diceSecret="gm"
|
|
596
|
+
\`\`\`
|
|
597
|
+
`;
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const _formatHP = (hp: number, maxhp: number) => {
|
|
601
|
+
const base32 = Math.ceil(hp * 32 / maxhp);
|
|
602
|
+
|
|
603
|
+
const slabs = Math.floor(base32 / 8);
|
|
604
|
+
const tail = base32 % 8;
|
|
605
|
+
|
|
606
|
+
const tails = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉'];
|
|
607
|
+
|
|
608
|
+
return `▕${new Array(slabs).fill(0).map(() => '█').join('')}${[tails[tail], ...new Array(3).fill(0).map(() => ' ')].slice(0, 4 - slabs).join('')}▏`;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export const formatHP = (arg: string) => {
|
|
612
|
+
const match = arg.match(/^(m)?(\d+)\/(\d+)$/);
|
|
613
|
+
|
|
614
|
+
if (!match) {
|
|
615
|
+
throw new Error('No match');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const negative = !!match[1];
|
|
619
|
+
const hp = parseInt(match[2]);
|
|
620
|
+
const maxhp = parseInt(match[3]);
|
|
621
|
+
|
|
622
|
+
return _formatHP(negative ? maxhp - hp : hp, maxhp);
|
|
623
|
+
};
|