pf2e-sage-stats 0.2.5 → 0.2.7

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
@@ -4,6 +4,7 @@ import { capitalize, entries, startCase, toLower, kebabCase, keys, difference, g
4
4
  import dedent from 'dedent-js';
5
5
  import abbreviate from 'abbreviate';
6
6
  import pluralize from 'pluralize';
7
+ import { JSDOM } from 'jsdom';
7
8
  import { readdir, rename, stat } from 'fs/promises';
8
9
 
9
10
  const schema = zod.object({
@@ -45,6 +46,8 @@ const schema = zod.object({
45
46
  stealth: zod.coerce.number().optional(),
46
47
  survival: zod.coerce.number().optional(),
47
48
  thievery: zod.coerce.number().optional(),
49
+ piloting: zod.coerce.number().optional(),
50
+ computers: zod.coerce.number().optional(),
48
51
  }).default({}),
49
52
  lores: zod.record(zod.string(), zod.object({
50
53
  mod: zod.coerce.number().default(0),
@@ -103,6 +106,8 @@ const schema = zod.object({
103
106
  stealth: zod.array(zod.string()).optional(),
104
107
  survival: zod.array(zod.string()).optional(),
105
108
  thievery: zod.array(zod.string()).optional(),
109
+ piloting: zod.array(zod.string()).optional(),
110
+ computers: zod.array(zod.string()).optional(),
106
111
  }).default({}),
107
112
  })
108
113
 
@@ -147,6 +152,8 @@ export const stub: Required<Schema> = {
147
152
  stealth: 0,
148
153
  survival: 0,
149
154
  thievery: 0,
155
+ piloting: 0,
156
+ computers: 0,
150
157
  },
151
158
  lores: {},
152
159
  melee: {
@@ -181,7 +188,7 @@ const childSchema = zod.object({
181
188
 
182
189
  export type ChildSchema = zod.infer<typeof childSchema>;
183
190
 
184
- const charSchema = zod.object({
191
+ export const charSchema = zod.object({
185
192
  init: zod.coerce.number().default(0),
186
193
  foe: zod.boolean().default(false),
187
194
  state: zod.union([zod.literal('empty'), zod.literal('arrow'), zod.literal('check'), zod.literal('cross')]).default('empty'),
@@ -197,7 +204,7 @@ const charSchema = zod.object({
197
204
 
198
205
  export type CharSchema = zod.infer<typeof charSchema>;
199
206
 
200
- export const prediceateMap: Record<string, string> = {
207
+ export const flatMap: Record<string, string> = {
201
208
  ...([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map((v): [string, string] => (
202
209
  [`${v}`, `d20 >= ${v} flat;`]
203
210
  )).reduce<Record<string, string>>((p, [k, v]) => { p[k] = v; return p }, {})),
@@ -207,14 +214,24 @@ export const prediceateMap: Record<string, string> = {
207
214
  hidden: 'd20 >= 11 flat;',
208
215
  };
209
216
 
210
- export const adjustmentMap: () => Record<string, string> = () => ({
211
- 'incredibly-easy': '`(Incredibly Easy)`',
212
- 'very-easy': '`(Very Easy)`',
213
- 'easy': '`(Easy)`',
214
- 'hard': '`(Hard)`',
215
- 'very-hard': '`(Very Easy)`',
216
- 'incredibly-hard': '`(Incredibly Hard)`',
217
- });
217
+ export const diceMap: Record<string, string> = {
218
+ ...([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map((v): [string, string] => (
219
+ [`${v}`, `(${v})`]
220
+ )).reduce<Record<string, string>>((p, [k, v]) => { p[k] = v; return p }, {})),
221
+ a: '+2',
222
+ adv: '+2',
223
+ advantage: '+2',
224
+ d: '-2',
225
+ dis: '-2',
226
+ disadvantage: '-2',
227
+ f: '+2',
228
+ for: '+2',
229
+ fort: '+2',
230
+ fortune: '+2',
231
+ m: '-2',
232
+ mis: '-2',
233
+ misfortune: '-2',
234
+ };
218
235
 
219
236
  export const fromatMap = (name: string, map: Record<string, string>) => {
220
237
  return TSV.stringify([{
@@ -308,10 +325,7 @@ const locateStrikes = (statblock: string, alias: string): Record<string, Schema[
308
325
  .replace(/\+\d/g, '')
309
326
  .replace(/Weapon\s+Striking(\s+\((Greater|Major)\))?/gi, '')
310
327
  .replace(/((Greater|Major)\s+)?Striking/gi, '')
311
- .replace(/\(Agile\)/gi, '')
312
- .replace(/\(Finesse\)/gi, '')
313
- .replace(/\(\+\)/gi, '')
314
- .replace(/\(\)/gi, '')
328
+ .replace(/\(.*\)/gi, '')
315
329
  .replace(/\s+/g, ' ')
316
330
  .trim();
317
331
 
@@ -333,6 +347,8 @@ const locateStrikes = (statblock: string, alias: string): Record<string, Schema[
333
347
  .replace(/Electricity/, 'electricity')
334
348
  .replace(/Sonic/, 'sonic')
335
349
  .replace(/Spirit/, 'spirit')
350
+ .replace(/Acid/, 'acid')
351
+ .replace(/[pP]lus\s(\d+d)/, '+$1')
336
352
  .trim();
337
353
 
338
354
  const twoHand = _traits.match(/(two-hand|two-handed)\s+(d\d+)/);
@@ -440,6 +456,8 @@ export const parseStatblock = (name: string | null, _statblock: string, alias: s
440
456
  ...locateInt('stealth', statblock, 'Skills'),
441
457
  ...locateInt('survival', statblock, 'Skills'),
442
458
  ...locateInt('thievery', statblock, 'Skills'),
459
+ ...locateInt('piloting', statblock, 'Skills'),
460
+ ...locateInt('computers', statblock, 'Skills'),
443
461
  },
444
462
  lores: {
445
463
  ...locateInts(statblock, 'Skills', 'Lore'),
@@ -484,18 +502,7 @@ const toRanged = ([key, value]: [string, Schema['ranged'][string]]): [string, nu
484
502
  ...(value.damage ? [[`ranged.${key}.damage`, value.damage] as [string, string]] : []),
485
503
  ];
486
504
 
487
- const recallDCs = (level: number, secret: boolean, bump: number): [string, string][] => ((value) => [
488
- ['dc.recall.incredibly-easy', secret ? `||${value - 10}||` : `${value - 10}`],
489
- ['dc.recall.very-easy', secret ? `||${value - 5}||` : `${value - 5}`],
490
- ['dc.recall.easy', secret ? `||${value - 2}||` : `${value - 2}`],
491
- ['dc.recall.default', secret ? `||${value}||` : `${value}`],
492
- ['dc.recall', secret ? `||${value}||` : `${value}`],
493
- ['dc.recall.hard', secret ? `||${value + 2}||` : `${value + 2}`],
494
- ['dc.recall.very-hard', secret ? `||${value + 5}||` : `${value + 5}`],
495
- ['dc.recall.incredibly-hard', secret ? `||${value + 10}||` : `${value + 10}`],
496
- ])(dcByLevel[Math.max(Math.min(level + 1, dcByLevel.length - 1), 0)] + bump);
497
-
498
- export const flatten = (stats: Schema, secretDC: boolean = false, defaultSkills: boolean = false, recallDC: boolean = false): Record<string, string | number> => {
505
+ export const flatten = (stats: Schema, secretDC: boolean = false, defaultSkills: boolean = false): Record<string, string | number> => {
499
506
  const untrained = stats.untrained;
500
507
  const skills = defaultSkills ? {
501
508
  acrobatics: untrained + stats.attributes.dexterity,
@@ -514,6 +521,8 @@ export const flatten = (stats: Schema, secretDC: boolean = false, defaultSkills:
514
521
  stealth: untrained + stats.attributes.dexterity,
515
522
  survival: untrained + stats.attributes.wisdom,
516
523
  thievery: untrained + stats.attributes.dexterity,
524
+ piloting: untrained + stats.attributes.dexterity,
525
+ computers: untrained + stats.attributes.intelligence,
517
526
  ...stats.skills,
518
527
  } : stats.skills;
519
528
 
@@ -600,9 +609,6 @@ export const flatten = (stats: Schema, secretDC: boolean = false, defaultSkills:
600
609
  ...entries(skills).map(mod2DC(true, (stats.bump.skills ?? stats.bump.default))),
601
610
  ...entries(stats.extra).map(mod2DC(true, stats.bump.default)),
602
611
  ] : []),
603
- ...(recallDC ? [
604
- ...recallDCs(stats.level, secretDC, stats.bump.default),
605
- ] : []),
606
612
  ...(stats.perceptionInfo.length > 0 ? [
607
613
  [`perception.info`, `\`(${stats.perceptionInfo.join(', ')})\``]
608
614
  ] : []),
@@ -615,12 +621,12 @@ export const flatten = (stats: Schema, secretDC: boolean = false, defaultSkills:
615
621
  ].reduce<Record<string | number, string | number>>((p, [key, value]) => { p[key] = value; return p }, {});
616
622
  }
617
623
 
618
- export const formatTSV = (stats: Schema, secretDC: boolean = false, defaultSkills: boolean = false, recallDC: boolean = false): string => (
619
- TSV.stringify([flatten(stats, secretDC, defaultSkills, recallDC)])
624
+ export const formatTSV = (stats: Schema, secretDC: boolean = false, defaultSkills: boolean = false): string => (
625
+ TSV.stringify([flatten(stats, secretDC, defaultSkills)])
620
626
  );
621
627
 
622
- export const formatCommand = (stats: Schema, secretDC: boolean = false, defaultSkills: boolean = false, recallDC: boolean = false): string => (
623
- Object.entries(flatten(stats, secretDC, defaultSkills, recallDC)).map(([k, v]) => `${k}="${v}"`).join(' ')
628
+ export const formatCommand = (stats: Schema, secretDC: boolean = false, defaultSkills: boolean = false): string => (
629
+ Object.entries(flatten(stats, secretDC, defaultSkills)).map(([k, v]) => `${k}="${v}"`).join(' ')
624
630
  );
625
631
 
626
632
  export const formatTable = (table: string, season: string, scenario: string, _name: string, gm: string, players: string) => {
@@ -634,7 +640,7 @@ export const formatTable = (table: string, season: string, scenario: string, _na
634
640
  @TableBot create "${gameName}" --gm ${gm} --players ${players} --table-name ${tableName} --ooc-table-name ooc-${tableName} --category Game Tables
635
641
  \`\`\`
636
642
  \`\`\`
637
- 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"
643
+ 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="Хронист"
638
644
  \`\`\`
639
645
  `;
640
646
  };
@@ -702,8 +708,11 @@ export const newtracker = (status: string) => {
702
708
  };
703
709
 
704
710
  export const formatTracker = (tracker: CharSchema[]) => {
711
+ const aliasSpace = (t: { alias: string }) => (t.alias.length > 0 ? 0 : 5);
712
+ const nameLengthWithAlias = (t: { name: string, alias: string }) => t.name.length - aliasSpace(t);
713
+
705
714
  const nameLength = tracker.reduce((p, c) => {
706
- const length = c.children.reduce((pp, cc) => pp > cc.name.length ? pp : cc.name.length, c.name.length);
715
+ const length = c.children.reduce((pp, cc) => pp > nameLengthWithAlias(cc) ? pp : nameLengthWithAlias(cc), nameLengthWithAlias(c));
707
716
 
708
717
  return p > length ? p : length;
709
718
  }, 0);
@@ -742,10 +751,12 @@ export const formatTracker = (tracker: CharSchema[]) => {
742
751
  ls.push('')
743
752
  }
744
753
 
745
- const hp = char.foe ? (
754
+ const hp = !char.foe && char.hp > 0 ? (
755
+ `${char.hp}${char.temphp > 0 ? `+${char.temphp}` : ''}/${char.maxhp}`.padStart(hpLength)
756
+ ) : char.hp > 0 ? (
746
757
  `-${char.maxhp - char.hp}${char.temphp > 0 ? `+${char.temphp}` : ''}`.padStart(hpLength)
747
758
  ) : (
748
- `${char.hp}${char.temphp > 0 ? `+${char.temphp}` : ''}/${char.maxhp}`.padStart(hpLength)
759
+ `${char.hp}${char.temphp > 0 ? `+${char.temphp}` : ''}`.padStart(hpLength)
749
760
  );
750
761
 
751
762
  const hpBar = (value: number, max: number) => (
@@ -759,7 +770,7 @@ export const formatTracker = (tracker: CharSchema[]) => {
759
770
  );
760
771
 
761
772
  ls.push((
762
- `${stateMap[char.state]}**\` ${char.name.padEnd(nameLength)} ▏${char.alias.padEnd(3)} ▏${hp} \`** ${hpBar(char.hp, char.maxhp)} `
773
+ `${stateMap[char.hp > 0 ? char.state : 'cross']}**\` ${char.name.padEnd(nameLength + aliasSpace(char))}${char.alias.length > 0 ? ` ▏${char.alias.padEnd(3)}` : ''} ▏${hp} \`** ${hpBar(char.hp, char.maxhp)} `
763
774
  ))
764
775
 
765
776
  if (char.conditions.length > 0) {
@@ -767,14 +778,16 @@ export const formatTracker = (tracker: CharSchema[]) => {
767
778
  }
768
779
 
769
780
  for (const child of char.children) {
770
- const hhp = char.foe ? (
781
+ const hhp = !char.foe && child.hp > 0 ? (
782
+ `${child.hp}${child.temphp > 0 ? `+${child.temphp}` : ''}/${child.maxhp}`.padStart(hpLength)
783
+ ) : child.hp > 0 ? (
771
784
  `-${child.maxhp - child.hp}${child.temphp > 0 ? `+${child.temphp}` : ''}`.padStart(hpLength)
772
785
  ) : (
773
- `${child.hp}${child.temphp > 0 ? `+${child.temphp}` : ''}/${child.maxhp}`.padStart(hpLength)
786
+ `${child.hp}${child.temphp > 0 ? `+${child.temphp}` : ''}`.padStart(hpLength)
774
787
  );
775
788
 
776
789
  ls.push((
777
- `:smallnode:**\` ${child.name.padEnd(nameLength)} ▏${child.alias.padEnd(3)} ▏${hhp} \`** ${hpBar(child.hp, child.maxhp)} `
790
+ `:smallnode:**\` ${child.name.padEnd(nameLength + aliasSpace(child))}${child.alias.length > 0 ? ` ▏${child.alias.padEnd(3)}` : ''} ▏${hhp} \`** ${hpBar(child.hp, child.maxhp)} `
778
791
  ))
779
792
 
780
793
  if (child.conditions.length > 0) {
@@ -813,3 +826,52 @@ export const sortFolder = async () => {
813
826
 
814
827
  return transformed;
815
828
  };
829
+
830
+ export const screenPlay = (htmlContent: string) => {
831
+ const dom = new JSDOM(htmlContent);
832
+ const document = dom.window.document;
833
+
834
+ const outputLines = [];
835
+
836
+ const preambleEntry = document.querySelector('.preamble__entry');
837
+ if (preambleEntry) {
838
+ outputLines.push(`SCENE: ${preambleEntry.textContent}`);
839
+ outputLines.push('');
840
+ }
841
+
842
+ // Process all chatlog messages
843
+ const messages = document.querySelectorAll('.chatlog__message');
844
+
845
+ messages.forEach(message => {
846
+ const authorElement = message.querySelector('.chatlog__author');
847
+ const contentElement = message.querySelector('.chatlog__content');
848
+
849
+ if (!authorElement || !contentElement) return;
850
+
851
+ const author = authorElement.textContent?.trim() ?? '';
852
+ const content = contentElement.textContent?.trim() ?? '';
853
+
854
+ // Skip bot tags in character names
855
+ const characterName = author.replace(/\s*BOT$/, '');
856
+
857
+ // Add character line
858
+ outputLines.push(`${characterName.toUpperCase()}`);
859
+
860
+ // Add dialogue/content (wrap long lines)
861
+ const lines = content.split('\n').map((c) => (
862
+ c
863
+ .replace(/(—|--)/g, '-')
864
+ // eslint-disable-next-line no-irregular-whitespace
865
+ .replace(/(​|​)/g, '')
866
+ .trim()
867
+ )).filter((c) => c.length);
868
+
869
+ for (const line of lines) {
870
+ outputLines.push(` ${line}`);
871
+ }
872
+
873
+ outputLines.push('');
874
+ });
875
+
876
+ return outputLines.join('\n');
877
+ }
package/src/index.ts CHANGED
@@ -4,10 +4,11 @@ import { Command } from 'commander';
4
4
  import fs from 'fs/promises';
5
5
  import path from 'path';
6
6
  import readline from 'readline';
7
+ import clipboardy from 'clipboardy';
7
8
 
8
9
  import pack from '../package.json';
9
10
 
10
- import { sortFolder, formatCommand, formatJSON, formatTSV, fromatMap, parseJSON, parseStatblock, stub, adjustmentMap, prediceateMap, formatTable, formatHP, newtracker, parseTracker, formatTracker } from './app';
11
+ import { sortFolder, formatCommand, formatJSON, formatTSV, fromatMap, parseJSON, parseStatblock, stub, flatMap, formatTable, formatHP, newtracker, parseTracker, formatTracker, screenPlay, diceMap, charSchema } from './app';
11
12
 
12
13
  const program = new Command(pack.name);
13
14
 
@@ -57,27 +58,41 @@ program.command('sort')
57
58
  console.log(await sortFolder());
58
59
  });
59
60
 
61
+ program.command('convert')
62
+ .description('Convert a discord export file into a txt log')
63
+ .argument('<file>', 'input file')
64
+ .option('-o, --output <file>', 'output file')
65
+ .action(async (argument, option) => {
66
+ const file = await fs.readFile(argument, { encoding: 'utf-8' });
67
+
68
+ if (option.output) {
69
+ await fs.writeFile(option.output, screenPlay(file), { encoding: 'utf-8' })
70
+ } else {
71
+ console.log(screenPlay(file));
72
+ }
73
+ });
74
+
60
75
  program.command('flat')
61
76
  .description('Generte a special flat npc tsv')
62
77
  .option('-n, --name <string>', 'npc name')
63
78
  .option('-o, --output <file>', 'output json')
64
79
  .action(async (option) => {
65
80
  if (option.output) {
66
- await fs.writeFile(option.output, fromatMap(option.name ?? 'flat', prediceateMap), { encoding: 'utf-8' })
81
+ await fs.writeFile(option.output, fromatMap(option.name ?? 'flat', flatMap), { encoding: 'utf-8' })
67
82
  } else {
68
- console.log(fromatMap(option.name ?? 'flat', prediceateMap));
83
+ console.log(fromatMap(option.name ?? 'flat', flatMap));
69
84
  }
70
85
  });
71
86
 
72
- program.command('adjustment')
73
- .description('Generte a special adjustment npc tsv')
87
+ program.command('dice')
88
+ .description('Generte a special dice npc tsv')
74
89
  .option('-n, --name <string>', 'npc name')
75
90
  .option('-o, --output <file>', 'output json')
76
91
  .action(async (option) => {
77
92
  if (option.output) {
78
- await fs.writeFile(option.output, fromatMap(option.name ?? 'adjustment', adjustmentMap()), { encoding: 'utf-8' })
93
+ await fs.writeFile(option.output, fromatMap(option.name ?? 'dice', diceMap), { encoding: 'utf-8' })
79
94
  } else {
80
- console.log(fromatMap(option.name ?? 'adjustment', adjustmentMap()));
95
+ console.log(fromatMap(option.name ?? 'dice', diceMap));
81
96
  }
82
97
  });
83
98
 
@@ -119,7 +134,6 @@ program.command('tsv')
119
134
  .argument('<file>', 'input json')
120
135
  .option('-o, --output <file>', 'output tsv')
121
136
  .option('-s, --secretDC', 'produce secret DCs')
122
- .option('-r, --recallDC', 'produce recall DCs')
123
137
  .option('-d, --defaultSkills', 'produce values for untrained skills')
124
138
  .action(async (argument, option) => {
125
139
  const file = await fs.readFile(argument, { encoding: 'utf-8' });
@@ -128,7 +142,7 @@ program.command('tsv')
128
142
 
129
143
  const output = option.output ? option.output : path.join(path.parse(argument).dir, path.parse(argument).name + '.tsv');
130
144
 
131
- await fs.writeFile(output, formatTSV(stats, option.secretDC, option.defaultSkills, option.recallDC), { encoding: 'utf-8' });
145
+ await fs.writeFile(output, formatTSV(stats, option.secretDC, option.defaultSkills), { encoding: 'utf-8' });
132
146
  });
133
147
 
134
148
  program.command('command')
@@ -136,7 +150,6 @@ program.command('command')
136
150
  .argument('<file>', 'input json')
137
151
  .option('-o, --output <file>', 'output txt')
138
152
  .option('-s, --secretDC', 'produce secret DCs')
139
- .option('-r, --recallDC', 'produce recall DCs')
140
153
  .option('-d, --defaultSkills', 'produce values for untrained skills')
141
154
  .action(async (argument, option) => {
142
155
  const file = await fs.readFile(argument, { encoding: 'utf-8' });
@@ -145,7 +158,7 @@ program.command('command')
145
158
 
146
159
  const output = option.output ? option.output : path.join(path.parse(argument).dir, path.parse(argument).name + '-command.txt');
147
160
 
148
- await fs.writeFile(output, formatCommand(stats, option.secretDC, option.defaultSkills, option.recallDC), { encoding: 'utf-8' });
161
+ await fs.writeFile(output, formatCommand(stats, option.secretDC, option.defaultSkills), { encoding: 'utf-8' });
149
162
  });
150
163
 
151
164
  program.command('newtracker')
@@ -193,7 +206,10 @@ program.command('track')
193
206
  await fs.writeFile(option.output, formatTracker(tracker), { encoding: 'utf-8' })
194
207
  } else {
195
208
  console.clear();
196
- console.log(formatTracker(tracker));
209
+
210
+ const track = formatTracker(tracker);
211
+ clipboardy.writeSync(track)
212
+ console.log(track);
197
213
  }
198
214
  }
199
215
 
@@ -224,6 +240,15 @@ program.command('track')
224
240
  }, {
225
241
  regex: /^\s*reset/i,
226
242
  map: () => ({ command: 'reset' as const })
243
+ }, {
244
+ regex: /^\s*purge/i,
245
+ map: () => ({ command: 'purge' as const })
246
+ }, {
247
+ regex: /^\s*blank/i,
248
+ map: () => ({ command: 'blank' as const })
249
+ }, {
250
+ regex: /^\s*foe(\s+(\S+))?(\s+(\S+))?(\s+(\d+))?/i,
251
+ map: (match: RegExpMatchArray) => ({ command: 'foe' as const, name: match[2], alias: match[4], hp: match[6] && parseInt(match[6], 10) })
227
252
  }, {
228
253
  regex: /^\s*heal/i,
229
254
  map: () => ({ command: 'heal' as const })
@@ -258,19 +283,73 @@ program.command('track')
258
283
  }
259
284
  },
260
285
  }, {
261
- regex: /^\s*(\S+)\s+[sS]\s*"(.*)"/,
286
+ regex: /^\s*([^\s="+-]+)\s*=?"([^"]*)"/,
262
287
  map: (match: RegExpMatchArray) => {
263
288
  return { command: 'set-conditions' as const, tag: match[1], value: match[2] };
264
289
  },
290
+ }, {
291
+ regex: /^\s*([^\s="+-]+)\s*\+"([^"]*)"/,
292
+ map: (match: RegExpMatchArray) => {
293
+ return { command: 'add-conditions' as const, tag: match[1], value: match[2] };
294
+ },
295
+ }, {
296
+ regex: /^\s*([^\s="+-]+)\s*-"([^"]*)"/,
297
+ map: (match: RegExpMatchArray) => {
298
+ return { command: 'rem-conditions' as const, tag: match[1], value: match[2] };
299
+ },
300
+ }, {
301
+ regex: /^\s*([^\s="+-]+)\s*\+\+"([^"]*)"/,
302
+ map: (match: RegExpMatchArray) => {
303
+ return { command: 'inc-conditions' as const, tag: match[1], value: match[2] };
304
+ },
305
+ }, {
306
+ regex: /^\s*([^\s="+-]+)\s*--"([^"]*)"/,
307
+ map: (match: RegExpMatchArray) => {
308
+ return { command: 'dec-conditions' as const, tag: match[1], value: match[2] };
309
+ },
310
+ }, {
311
+ regex: /^\s*(\S+)\s+[sS]?\s*=?"([^"]*)"/,
312
+ map: (match: RegExpMatchArray) => {
313
+ return { command: 'set-conditions' as const, tag: match[1], value: match[2] };
314
+ },
315
+ }, {
316
+ regex: /^\s*(\S+)\s+[sS]?\s*\+"([^"]*)"/,
317
+ map: (match: RegExpMatchArray) => {
318
+ return { command: 'add-conditions' as const, tag: match[1], value: match[2] };
319
+ },
320
+ }, {
321
+ regex: /^\s*(\S+)\s+[sS]?\s*-"([^"]*)"/,
322
+ map: (match: RegExpMatchArray) => {
323
+ return { command: 'rem-conditions' as const, tag: match[1], value: match[2] };
324
+ },
325
+ }, {
326
+ regex: /^\s*(\S+)\s+[sS]?\s*\+\+"([^"]*)"/,
327
+ map: (match: RegExpMatchArray) => {
328
+ return { command: 'inc-conditions' as const, tag: match[1], value: match[2] };
329
+ },
330
+ }, {
331
+ regex: /^\s*(\S+)\s+[sS]?\s*--"([^"]*)"/,
332
+ map: (match: RegExpMatchArray) => {
333
+ return { command: 'dec-conditions' as const, tag: match[1], value: match[2] };
334
+ },
265
335
  }, {
266
336
  regex: /^\s*(\S+)\s+[cC]/,
267
337
  map: (match: RegExpMatchArray) => ({ command: 'check' as const, tag: match[1] }),
268
338
  }, {
269
339
  regex: /^\s*(\S+)\s+[aA]/,
270
340
  map: (match: RegExpMatchArray) => ({ command: 'arrow' as const, tag: match[1] }),
341
+ }, {
342
+ regex: /^\s*(\S+)\s+[eE]/,
343
+ map: (match: RegExpMatchArray) => ({ command: 'empty' as const, tag: match[1] }),
344
+ }, {
345
+ regex: /^\s*(\S+)\s+[xX]/,
346
+ map: (match: RegExpMatchArray) => ({ command: 'cross' as const, tag: match[1] }),
271
347
  }, {
272
348
  regex: /^\s*(\S+)\s+[dD]/,
273
349
  map: (match: RegExpMatchArray) => ({ command: 'delete' as const, tag: match[1] }),
350
+ }, {
351
+ regex: /^\s*(\S+)\s+[wW]/,
352
+ map: (match: RegExpMatchArray) => ({ command: 'wait' as const, tag: match[1] }),
274
353
  }];
275
354
 
276
355
  const actions: ReturnType<typeof commands[number]['map']>[] = [];
@@ -322,6 +401,26 @@ program.command('track')
322
401
  blocks[i].forEach((b) => b.state = 'empty')
323
402
  }
324
403
  }
404
+ } else if (action.command === 'wait') {
405
+ const sorted = [...tracker].sort((a, b) => (a.init === b.init) ? (b.foe ? 1 : 0) - (a.foe ? 1 : 0) : b.init - a.init);
406
+
407
+ const blocks = [[sorted[0]]];
408
+
409
+ for (let i=1; i < sorted.length; ++i) {
410
+ if (sorted[i].foe === blocks[blocks.length-1][0].foe) {
411
+ blocks[blocks.length-1].push(sorted[i])
412
+ } else {
413
+ blocks.push([sorted[i]])
414
+ }
415
+ }
416
+
417
+ const blockIndex = blocks.findIndex((b) => b.find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag))
418
+ const block = blocks[blockIndex + 1]
419
+ const item = tracker.find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag)
420
+
421
+ if (block && item) {
422
+ item.init = (block[block.length - 1]?.init ?? 0) - 1;
423
+ }
325
424
  } else if (action.command === 'check') {
326
425
  const item = tracker.find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag)
327
426
 
@@ -334,11 +433,25 @@ program.command('track')
334
433
  if (item) {
335
434
  item.state = "arrow"
336
435
  }
436
+ } else if (action.command === 'empty') {
437
+ const item = tracker.find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag)
438
+
439
+ if (item) {
440
+ item.state = "empty"
441
+ }
442
+ } else if (action.command === 'cross') {
443
+ const item = tracker.find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag)
444
+
445
+ if (item) {
446
+ item.state = "cross"
447
+ }
337
448
  } else if (action.command === 'delete') {
338
449
  tracker = tracker.filter((c) => !(c.id === action.tag || c.alias === action.tag || c.name == action.tag)).map((p) => ({
339
450
  ...p,
340
451
  children: p.children.filter((c) => !(c.id === action.tag || c.alias === action.tag || c.name == action.tag)),
341
452
  }))
453
+ } else if (action.command === 'purge') {
454
+ tracker = tracker.filter((c) => !c.foe)
342
455
  } else if (action.command === 'exit') {
343
456
  process.exit(0)
344
457
  } else if (action.command === 'heal') {
@@ -375,13 +488,81 @@ program.command('track')
375
488
  if (item) {
376
489
  item.init = Math.max(0, action.value)
377
490
  }
491
+ } else if (action.command === 'blank') {
492
+ tracker.forEach((item) => {
493
+ item.conditions = ""
494
+ })
378
495
  } else if (action.command === 'set-conditions') {
379
496
  const item = tracker.find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag) ?? (
380
497
  tracker.flatMap((c) => c.children).find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag)
381
498
  )
382
499
 
383
500
  if (item) {
384
- item.conditions = action.value
501
+ item.conditions = action.value.trim()
502
+ }
503
+ } else if (action.command === 'add-conditions') {
504
+ const item = tracker.find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag) ?? (
505
+ tracker.flatMap((c) => c.children).find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag)
506
+ )
507
+
508
+ if (item) {
509
+ const conditions = [...item.conditions.split(',').map((c) => c.toLowerCase().trim()).filter((c) => c.length), action.value.toLowerCase().trim()];
510
+
511
+ item.conditions = conditions.join(', ')
512
+ }
513
+ } else if (action.command === 'rem-conditions') {
514
+ const item = tracker.find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag) ?? (
515
+ tracker.flatMap((c) => c.children).find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag)
516
+ )
517
+
518
+ if (item) {
519
+ const conditions = item.conditions.split(',').map((c) => c.toLowerCase().trim()).filter((c) => c.length).filter((c) => !c.includes(action.value.toLowerCase().trim()));
520
+
521
+ item.conditions = conditions.join(', ')
522
+ }
523
+ } else if (action.command === 'inc-conditions') {
524
+ const item = tracker.find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag) ?? (
525
+ tracker.flatMap((c) => c.children).find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag)
526
+ )
527
+
528
+ if (item) {
529
+ const conditions = item.conditions.split(',').map((c) => c.toLowerCase().trim()).filter((c) => c.length).map((c) => {
530
+ if (c.includes(action.value.toLowerCase().trim())) {
531
+ const match = c.match(/(.*)\s+(\d+)/);
532
+
533
+ if (match && match[1] && match[2]) {
534
+ return `${match[1].trim()} ${parseInt(match[2]) + 1}`
535
+ }
536
+ }
537
+
538
+ return c;
539
+ });
540
+
541
+ item.conditions = conditions.join(', ')
542
+ }
543
+ } else if (action.command === 'dec-conditions') {
544
+ const item = tracker.find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag) ?? (
545
+ tracker.flatMap((c) => c.children).find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag)
546
+ )
547
+
548
+ if (item) {
549
+ const conditions = item.conditions.split(',').map((c) => c.toLowerCase().trim()).filter((c) => c.length).map((c) => {
550
+ if (c.includes(action.value.toLowerCase().trim())) {
551
+ const match = c.match(/(.*)\s+(\d+)/);
552
+
553
+ if (match && match[1] && match[2]) {
554
+ if (parseInt(match[2]) > 1) {
555
+ return `${match[1].trim()} ${parseInt(match[2]) - 1}`
556
+ } else {
557
+ return match[1].trim()
558
+ }
559
+ }
560
+ }
561
+
562
+ return c;
563
+ });
564
+
565
+ item.conditions = conditions.join(', ')
385
566
  }
386
567
  } else if (action.command === 'mod-thp') {
387
568
  const item = tracker.find((c) => c.id === action.tag || c.alias === action.tag || c.name == action.tag) ?? (
@@ -416,6 +597,8 @@ program.command('track')
416
597
  if (item) {
417
598
  item.init = Math.max(0, item.init + action.value)
418
599
  }
600
+ } else if (action.command === 'foe') {
601
+ tracker.push(charSchema.parse({ foe: true, name: action.name, alias: action.alias, maxhp: action.hp, hp: action.hp }))
419
602
  }
420
603
  }
421
604