pf2e-sage-stats 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/app.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import zod from 'zod';
2
2
  import { TSV } from 'tsv';
3
- import { capitalize, entries, startCase, toLower, kebabCase } from 'lodash';
3
+ import { capitalize, entries, startCase, toLower, kebabCase, keys, difference, groupBy, mapValues } from 'lodash';
4
4
  import dedent from 'dedent-js';
5
5
  import abbreviate from 'abbreviate';
6
6
  import pluralize from 'pluralize';
7
+ import { readdir, rename, stat } from 'fs/promises';
7
8
 
8
9
  const schema = zod.object({
9
10
  name: zod.string(),
@@ -25,6 +26,8 @@ const schema = zod.object({
25
26
  }).default({}),
26
27
  ac: zod.coerce.number().default(10),
27
28
  perception: zod.coerce.number().default(0),
29
+ perceptionInfo: zod.array(zod.string()).default([]),
30
+ initiativeInfo: zod.array(zod.string()).default([]),
28
31
  skills: zod.object({
29
32
  acrobatics: zod.coerce.number().optional(),
30
33
  arcana: zod.coerce.number().optional(),
@@ -36,7 +39,7 @@ const schema = zod.object({
36
39
  medicine: zod.coerce.number().optional(),
37
40
  nature: zod.coerce.number().optional(),
38
41
  occultism: zod.coerce.number().optional(),
39
- perfomance: zod.coerce.number().optional(),
42
+ performance: zod.coerce.number().optional(),
40
43
  religion: zod.coerce.number().optional(),
41
44
  society: zod.coerce.number().optional(),
42
45
  stealth: zod.coerce.number().optional(),
@@ -50,12 +53,12 @@ const schema = zod.object({
50
53
  melee: zod.record(zod.string(), zod.object({
51
54
  mod: zod.coerce.number().default(0),
52
55
  desc: zod.string(),
53
- damage: zod.string(),
56
+ damage: zod.string().optional(),
54
57
  })).default({}),
55
58
  ranged: zod.record(zod.string(), zod.object({
56
59
  mod: zod.coerce.number().default(0),
57
60
  desc: zod.string(),
58
- damage: zod.string(),
61
+ damage: zod.string().optional(),
59
62
  })).default({}),
60
63
  spells: zod.object({
61
64
  attack: zod.coerce.number().optional(),
@@ -64,7 +67,42 @@ const schema = zod.object({
64
67
  extra: zod.record(zod.string(), zod.coerce.number()).default({}),
65
68
  extraDCs: zod.record(zod.string(), zod.coerce.number()).default({}),
66
69
  extraDice: zod.record(zod.string(), zod.coerce.string()).default({}),
67
- improvisation: zod.boolean().default(false),
70
+ classDC: zod.coerce.number().default(10),
71
+ untrained: zod.coerce.number().default(0),
72
+ bump: zod.object({
73
+ default: zod.coerce.number().default(0),
74
+ attack: zod.coerce.number().optional(),
75
+ ac: zod.coerce.number().optional(),
76
+ saves: zod.coerce.number().optional(),
77
+ skills: zod.coerce.number().optional(),
78
+ spells: zod.coerce.number().optional(),
79
+ perception: zod.coerce.number().optional(),
80
+ hp: zod.coerce.number().optional(),
81
+ }).default({ default: 0 }),
82
+ bullwark: zod.coerce.number().default(0),
83
+ saveInfo: zod.object({
84
+ fortitude: zod.array(zod.string()).default([]),
85
+ reflex: zod.array(zod.string()).default([]),
86
+ will: zod.array(zod.string()).default([]),
87
+ }).default({}),
88
+ skillInfo: zod.object({
89
+ acrobatics: zod.array(zod.string()).optional(),
90
+ arcana: zod.array(zod.string()).optional(),
91
+ athletics: zod.array(zod.string()).optional(),
92
+ crafting: zod.array(zod.string()).optional(),
93
+ deception: zod.array(zod.string()).optional(),
94
+ diplomacy: zod.array(zod.string()).optional(),
95
+ intimidation: zod.array(zod.string()).optional(),
96
+ medicine: zod.array(zod.string()).optional(),
97
+ nature: zod.array(zod.string()).optional(),
98
+ occultism: zod.array(zod.string()).optional(),
99
+ performance: zod.array(zod.string()).optional(),
100
+ religion: zod.array(zod.string()).optional(),
101
+ society: zod.array(zod.string()).optional(),
102
+ stealth: zod.array(zod.string()).optional(),
103
+ survival: zod.array(zod.string()).optional(),
104
+ thievery: zod.array(zod.string()).optional(),
105
+ }).default({}),
68
106
  })
69
107
 
70
108
  export type Schema = zod.infer<typeof schema>;
@@ -89,6 +127,8 @@ export const stub: Required<Schema> = {
89
127
  },
90
128
  ac: 10,
91
129
  perception: 0,
130
+ perceptionInfo: [],
131
+ initiativeInfo: [],
92
132
  skills: {
93
133
  acrobatics: 0,
94
134
  arcana: 0,
@@ -100,7 +140,7 @@ export const stub: Required<Schema> = {
100
140
  medicine: 0,
101
141
  nature: 0,
102
142
  occultism: 0,
103
- perfomance: 0,
143
+ performance: 0,
104
144
  religion: 0,
105
145
  society: 0,
106
146
  stealth: 0,
@@ -116,17 +156,16 @@ export const stub: Required<Schema> = {
116
156
  extra: {},
117
157
  extraDCs: {},
118
158
  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',
159
+ classDC: 10,
160
+ untrained: 0,
161
+ bump: { default: 0 },
162
+ bullwark: 0,
163
+ saveInfo: {
164
+ fortitude: [],
165
+ reflex: [],
166
+ will: [],
167
+ },
168
+ skillInfo: {},
130
169
  };
131
170
 
132
171
  export const prediceateMap: Record<string, string> = {
@@ -137,97 +176,15 @@ export const prediceateMap: Record<string, string> = {
137
176
  concealed: 'd20 >= 5 flat;',
138
177
  h: 'd20 >= 11 flat;',
139
178
  hidden: 'd20 >= 11 flat;',
140
- none: '',
141
179
  };
142
180
 
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
181
  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⟯',
182
+ 'incredibly-easy': '`(Incredibly Easy)`',
183
+ 'very-easy': '`(Very Easy)`',
184
+ 'easy': '`(Easy)`',
185
+ 'hard': '`(Hard)`',
186
+ 'very-hard': '`(Very Easy)`',
187
+ 'incredibly-hard': '`(Incredibly Hard)`',
231
188
  });
232
189
 
233
190
  export const fromatMap = (name: string, map: Record<string, string>) => {
@@ -317,16 +274,20 @@ const locateStrikes = (statblock: string, alias: string): Record<string, Schema[
317
274
 
318
275
  if (name && attack && dice) {
319
276
  const _name = name.toLowerCase()
320
- .replace(/\s+/g, ' ')
321
277
  .replace(/\+\d/g, '')
322
278
  .replace(/Weapon\s+Striking(\s+\((Greater|Major)\))?/gi, '')
279
+ .replace(/((Greater|Major)\s+)?Striking/gi, '')
323
280
  .replace(/\(Agile\)/gi, '')
281
+ .replace(/\(Finesse\)/gi, '')
324
282
  .replace(/\(\+\)/gi, '')
283
+ .replace(/\(\)/gi, '')
284
+ .replace(/\s+/g, ' ')
325
285
  .trim();
326
286
 
327
287
  const bps = /(?<=[^\w']|^)[BbPpSs](?=[^\w]|$)/g;
328
288
 
329
289
  let _traits = traits && traits.toLowerCase()
290
+ .replace(/\(\)/gi, '')
330
291
  .replace(/\s+/g, ' ')
331
292
  .replace(bps, (m) => m.toUpperCase())
332
293
  .replace(/1d/, 'd')
@@ -335,6 +296,9 @@ const locateStrikes = (statblock: string, alias: string): Record<string, Schema[
335
296
  const _dice = dice
336
297
  .replace(/\s+/g, ' ')
337
298
  .replace(bps, (m) => ({ b: 'bludgeoning', p: 'piercing', s: 'slashing' }[m.toLowerCase()] ?? m))
299
+ .replace(/Fire/, 'fire')
300
+ .replace(/Cold/, 'cold')
301
+ .replace(/Electricity/, 'electricity')
338
302
  .trim();
339
303
 
340
304
  const twoHand = _traits.match(/(two-hand|two-handed)\s+(d\d+)/);
@@ -389,15 +353,17 @@ const locateSpells = (statblock: string): Schema['spells'] | null => {
389
353
  export const parseStatblock = (name: string | null, _statblock: string, alias: string | null): Schema => {
390
354
  const defaults = schema.parse({ name: name ?? 'Unknown' });
391
355
 
356
+ const improvisation = _statblock.match(/Untrained\s+Improvisation/);
357
+ const plate = _statblock.match(/Full\s+Plate/)
358
+
392
359
  const statblock = _statblock
393
360
  .replace(/^Items.*$/m, '')
361
+ .replace('', '')
394
362
  .replace(/–/g, '-') + '\n';
395
363
 
396
- const improvisation = !!statblock.match(/Untrained\s+Improvisation/);
397
-
398
364
  const basic = locateName(statblock);
399
365
 
400
- return {
366
+ const result: Schema = {
401
367
  ...defaults,
402
368
  ...basic,
403
369
  ...(name && { name: name }),
@@ -434,7 +400,7 @@ export const parseStatblock = (name: string | null, _statblock: string, alias: s
434
400
  ...locateInt('medicine', statblock, 'Skills'),
435
401
  ...locateInt('nature', statblock, 'Skills'),
436
402
  ...locateInt('occultism', statblock, 'Skills'),
437
- ...locateInt('perfomance', statblock, 'Skills'),
403
+ ...locateInt('performance', statblock, 'Skills'),
438
404
  ...locateInt('religion', statblock, 'Skills'),
439
405
  ...locateInt('society', statblock, 'Skills'),
440
406
  ...locateInt('stealth', statblock, 'Skills'),
@@ -459,26 +425,31 @@ export const parseStatblock = (name: string | null, _statblock: string, alias: s
459
425
  ...locateInt('stealth', statblock, null, false, 'Stealth DC'),
460
426
  },
461
427
  extraDice: {},
462
- improvisation,
463
428
  };
429
+
430
+ result.untrained = improvisation ? (result.level >= 7 ? result.level : result.level >= 5 ? result.level - 1 : result.level - 2) : 0;
431
+ result.bullwark = plate ? Math.max(3 - result.attributes.dexterity, 0) : 0;
432
+
433
+ return result;
464
434
  }
465
435
 
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]];
436
+ const dc2DC = (secret: boolean, bump: number) => ([key, value]: [string | number, number]): [string, string] => [`dc.${key}`, secret ? `||${value + bump}||` : `${value + bump}`];
437
+ const mod2DC = (secret: boolean, bump: number) => ([key, value]: [string | number, number]): [string, string] => [`dc.${key}`, secret ? `||${10 + value + bump}||` : `${10 + value + bump}`];
438
+ const toLoreMod = ([key, value]: [string | number, { mod: number; name: string; }]): [string, number] => [`lore.${key}`, value.mod];
439
+ const toLoreName = ([key, value]: [string | number, { mod: number; name: string; }]): [string, string] => [`lore.${key}.name`, value.name];
469
440
  const toMelee = ([key, value]: [string, Schema['melee'][string]]): [string, number | string][] => [
470
441
  [`melee.${key}`, value.mod],
471
442
  [`melee.${key}.desc`, value.desc],
472
- [`melee.${key}.damage`, value.damage],
443
+ ...(value.damage ? [[`melee.${key}.damage`, value.damage] as [string, string]] : []),
473
444
  ];
474
445
 
475
446
  const toRanged = ([key, value]: [string, Schema['ranged'][string]]): [string, number | string][] => [
476
447
  [`ranged.${key}`, value.mod],
477
448
  [`ranged.${key}.desc`, value.desc],
478
- [`ranged.${key}.damage`, value.damage],
449
+ ...(value.damage ? [[`ranged.${key}.damage`, value.damage] as [string, string]] : []),
479
450
  ];
480
451
 
481
- const recallDCs = (level: number, secret: boolean): [string, string][] => ((value) => [
452
+ const recallDCs = (level: number, secret: boolean, bump: number): [string, string][] => ((value) => [
482
453
  ['dc.recall.incredibly-easy', secret ? `||${value - 10}||` : `${value - 10}`],
483
454
  ['dc.recall.very-easy', secret ? `||${value - 5}||` : `${value - 5}`],
484
455
  ['dc.recall.easy', secret ? `||${value - 2}||` : `${value - 2}`],
@@ -487,10 +458,10 @@ const recallDCs = (level: number, secret: boolean): [string, string][] => ((valu
487
458
  ['dc.recall.hard', secret ? `||${value + 2}||` : `${value + 2}`],
488
459
  ['dc.recall.very-hard', secret ? `||${value + 5}||` : `${value + 5}`],
489
460
  ['dc.recall.incredibly-hard', secret ? `||${value + 10}||` : `${value + 10}`],
490
- ])(dcByLevel[Math.max(Math.min(level + 1, dcByLevel.length - 1), 0)]);
461
+ ])(dcByLevel[Math.max(Math.min(level + 1, dcByLevel.length - 1), 0)] + bump);
491
462
 
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;
463
+ export const flatten = (stats: Schema, secretDC: boolean = false, defaultSkills: boolean = false, recallDC: boolean = false): Record<string, string | number> => {
464
+ const untrained = stats.untrained;
494
465
  const skills = defaultSkills ? {
495
466
  acrobatics: untrained + stats.attributes.dexterity,
496
467
  arcana: untrained + stats.attributes.intelligence,
@@ -502,7 +473,7 @@ export const flatten = (stats: Schema, secretDC: boolean = false, defaultSkills:
502
473
  medicine: untrained + stats.attributes.wisdom,
503
474
  nature: untrained + stats.attributes.wisdom,
504
475
  occultism: untrained + stats.attributes.intelligence,
505
- perfomance: untrained + stats.attributes.charisma,
476
+ performance: untrained + stats.attributes.charisma,
506
477
  religion: untrained + stats.attributes.wisdom,
507
478
  society: untrained + stats.attributes.intelligence,
508
479
  stealth: untrained + stats.attributes.dexterity,
@@ -511,7 +482,11 @@ export const flatten = (stats: Schema, secretDC: boolean = false, defaultSkills:
511
482
  ...stats.skills,
512
483
  } : stats.skills;
513
484
 
514
- let melee = entries(stats.melee);
485
+ let melee = entries(stats.melee).map(([k, v]): [string, typeof v] => [k, {
486
+ mod: v.mod + (stats.bump.attack ?? stats.bump.default),
487
+ desc: v.desc,
488
+ damage: stats.bump.default > 0 ? `${v.damage} +${stats.bump.default}` : v.damage,
489
+ }]);
515
490
  if (melee.length > 0) {
516
491
  melee = [
517
492
  ['default', melee[0][1]],
@@ -519,7 +494,11 @@ export const flatten = (stats: Schema, secretDC: boolean = false, defaultSkills:
519
494
  ];
520
495
  }
521
496
 
522
- let ranged = entries(stats.ranged);
497
+ let ranged = entries(stats.ranged).map(([k, v]): [string, typeof v] => [k, {
498
+ mod: v.mod + (stats.bump.attack ?? stats.bump.default),
499
+ desc: v.desc,
500
+ damage: v.damage && stats.bump.default > 0 ? `${v.damage} +${stats.bump.default}` : v.damage,
501
+ }]);
523
502
  if (ranged.length > 0) {
524
503
  ranged = [
525
504
  ['default', ranged[0][1]],
@@ -527,61 +506,79 @@ export const flatten = (stats: Schema, secretDC: boolean = false, defaultSkills:
527
506
  ];
528
507
  }
529
508
 
509
+ const maxhp = stats.bump.default > 0 ? (stats.bump.hp ? (stats.bump.hp + stats.maxhp) : Math.max(Math.floor(stats.maxhp * 1.1), stats.maxhp + 10)) : stats.maxhp;
510
+
511
+ const saveInfo = {
512
+ ...stats.saveInfo,
513
+ reflex: stats.bullwark > 0 ? [...stats.saveInfo.reflex, `+${stats.bullwark} more vs damage`] : stats.saveInfo.reflex,
514
+ }
515
+
516
+ const untrainedSkillInfo = entries(mapValues(groupBy(difference(keys(skills), keys(stats.skills)), (k) => k), () => ['untrained']));
517
+ const specifiedSkillInfo = entries(stats.skillInfo);
518
+
519
+ const skillInfo = mapValues(groupBy([...untrainedSkillInfo, ...specifiedSkillInfo], ([k]) => k), (list) => list.reduce<string[]>((p, [, v]) => [...p, ...v], []))
520
+
530
521
  return [
531
522
  ['name', stats.name],
532
523
  ...(stats.alias ? [['alias', stats.alias]] : []),
533
524
  ['gamesystem', 'pf2e'],
534
525
  ['level', stats.level],
535
526
  ['lvl', stats.level],
536
- ['maxhp', stats.maxhp],
527
+ ['hp', maxhp],
528
+ ['maxhp', maxhp],
537
529
  ...entries(stats.attributes),
538
530
  ...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
- ]: []),
531
+ ['ac', secretDC ? `||${stats.ac + (stats.bump.ac ?? stats.bump.default)}||` : stats.ac + (stats.bump.ac ?? stats.bump.default)],
532
+ ...entries(stats.saves).map(([k, v]) => [k, v + (stats.bump.saves ?? stats.bump.default)]),
533
+ ['fort', stats.saves.fortitude + (stats.bump.saves ?? stats.bump.default)],
534
+ ['ref', stats.saves.reflex + (stats.bump.saves ?? stats.bump.default)],
535
+ ['perception', stats.perception + (stats.bump.perception ?? stats.bump.default)],
536
+ ...entries(skills.performance ? { ...skills, perfomance: skills.performance } : skills).map(([k, v]) => [k, v + (stats.bump.skills ?? stats.bump.default)]),
537
+ ...entries(stats.lores).map(toLoreMod).map(([k, v]) => [k, v + (stats.bump.skills ?? stats.bump.default)]),
538
+ ...entries(stats.lores).map(toLoreName),
555
539
  ...melee.flatMap(toMelee),
556
540
  ...ranged.flatMap(toRanged),
557
541
  ...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)),
542
+ ...(stats.spells.attack ? [['spells', stats.spells.attack + (stats.bump.attack ?? stats.bump.default)]] : []),
543
+ ...(stats.spells.dc ? [dc2DC(secretDC, stats.bump.spells ?? stats.bump.default)(['spells', stats.spells.dc])] : []),
544
+ ...(stats.classDC ? [['classDC', secretDC ? `||${stats.classDC + stats.bump.default}||` : `${stats.classDC + stats.bump.default}`]] : []),
545
+ ...(stats.classDC ? [dc2DC(secretDC, stats.bump.default)(['class', stats.classDC])] : []),
546
+ dc2DC(secretDC, stats.bump.default)(['level', dcByLevel[Math.max(Math.min(stats.level + 1, dcByLevel.length - 1), 0)]]),
547
+ dc2DC(secretDC, stats.bump.ac ?? stats.bump.default)(['ac', stats.ac]),
548
+ ...entries(stats.extraDCs).map(dc2DC(secretDC, stats.bump.default)),
561
549
  ...entries(stats.extraDice),
562
550
  ...(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)),
551
+ ...entries(stats.saves).map(mod2DC(true, (stats.bump.saves ?? stats.bump.default))),
552
+ mod2DC(true, stats.bump.default)(['perception', stats.perception]),
553
+ mod2DC(true, (stats.bump.saves ?? stats.bump.default))(['fort', stats.saves.fortitude]),
554
+ mod2DC(true, (stats.bump.saves ?? stats.bump.default))(['ref', stats.saves.reflex]),
555
+ ...entries(skills).map(mod2DC(true, (stats.bump.skills ?? stats.bump.default))),
556
+ ...entries(stats.extra).map(mod2DC(true, stats.bump.default)),
569
557
  ] : []),
570
558
  ...(recallDC ? [
571
- ...recallDCs(stats.level, secretDC),
559
+ ...recallDCs(stats.level, secretDC, stats.bump.default),
560
+ ] : []),
561
+ ...(stats.perceptionInfo.length > 0 ? [
562
+ [`perception.info`, `\`(${stats.perceptionInfo.join(', ')})\``]
572
563
  ] : []),
564
+ ...(stats.initiativeInfo.length > 0 ? [
565
+ [`initiative.info`, `\`(${stats.initiativeInfo.join(', ')})\``]
566
+ ] : []),
567
+ ...entries(saveInfo).filter(([, v]) => v.length > 0).map(([k, v]) => [`${k}.info`, `\`(${v.join(', ')})\``]),
568
+ ...entries(skillInfo).filter(([, v]) => v.length > 0).map(([k, v]) => [`${k}.info`, `\`(${v.join(', ')})\``]),
569
+ ['out', secretDC ? 'xs' : 'm'],
573
570
  ].reduce<Record<string | number, string | number>>((p, [key, value]) => { p[key] = value; return p }, {});
574
571
  }
575
572
 
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)])
573
+ export const formatTSV = (stats: Schema, secretDC: boolean = false, defaultSkills: boolean = false, recallDC: boolean = false): string => (
574
+ TSV.stringify([flatten(stats, secretDC, defaultSkills, recallDC)])
578
575
  );
579
576
 
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(' ')
577
+ export const formatCommand = (stats: Schema, secretDC: boolean = false, defaultSkills: boolean = false, recallDC: boolean = false): string => (
578
+ Object.entries(flatten(stats, secretDC, defaultSkills, recallDC)).map(([k, v]) => `${k}="${v}"`).join(' ')
582
579
  );
583
580
 
584
- export const formatTable = (table: number, season: number, scenario: number, _name: string, gm: string, players: string) => {
581
+ export const formatTable = (table: string, season: string, scenario: string, _name: string, gm: string, players: string) => {
585
582
  const name = _name.replace(/[\s-_]/g, ' ').replace(/[^a-zA-Z0-9 ]/g, '');
586
583
 
587
584
  const gameName = `PF2 T${table} S${season} ${String(scenario).padStart(2, '0')} ${name}`;
@@ -621,3 +618,27 @@ export const formatHP = (arg: string) => {
621
618
 
622
619
  return _formatHP(negative ? maxhp - hp : hp, maxhp);
623
620
  };
621
+
622
+ export const sortFolder = async () => {
623
+ const dir = await readdir('.');
624
+
625
+ const summary = await Promise.all(dir.map(async (file) => {
626
+ const stats = await stat(file);
627
+
628
+ const timestamp = parseInt(stats.birthtimeMs.toFixed(0));
629
+
630
+ return { file, timestamp };
631
+ }))
632
+
633
+ const transformed = summary.sort((a, b) => a.timestamp - b.timestamp).map((s, index) => {
634
+ const prefix = String(index + 1).padStart(3, '0');
635
+
636
+ const name = s.file.replace('RU PFS Online - ', '').replace(/\s\[.+\]/, '');
637
+
638
+ return { oldname: s.file, newname: name.startsWith('ooc') ? name : `${prefix} ${name}` };
639
+ })
640
+
641
+ await Promise.all(transformed.map(async (t) => rename(t.oldname, t.newname)));
642
+
643
+ return transformed;
644
+ };