-
Notifications
You must be signed in to change notification settings - Fork 2.9k
/
Copy pathdatasearch.ts
3070 lines (2900 loc) · 117 KB
/
datasearch.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* Data searching commands.
* Pokemon Showdown - http://pokemonshowdown.com/
*
* Commands for advanced searching for pokemon, moves, items and learnsets.
* These commands run on a child process by default.
*
* @license MIT
*/
import { ProcessManager, Utils } from '../../lib';
import type { FormatData } from '../../sim/dex-formats';
import { TeamValidator } from '../../sim/team-validator';
import { Chat } from '../chat';
interface DexOrGroup {
abilities: { [k: string]: boolean };
tiers: { [k: string]: boolean };
doublesTiers: { [k: string]: boolean };
colors: { [k: string]: boolean };
'egg groups': { [k: string]: boolean };
formes: { [k: string]: boolean };
gens: { [k: string]: boolean };
moves: { [k: string]: boolean };
types: { [k: string]: boolean };
resists: { [k: string]: boolean };
weak: { [k: string]: boolean };
stats: { [k: string]: { [k in Direction]: { [s: string]: number | boolean } } };
skip: boolean;
}
interface MoveOrGroup {
types: { [k: string]: boolean };
categories: { [k: string]: boolean };
contestTypes: { [k: string]: boolean };
flags: { [k: string]: boolean };
gens: { [k: string]: boolean };
other: { [k: string]: boolean };
mon: { [k: string]: boolean };
property: { [k: string]: { [k in Direction]: number } };
boost: { [k: string]: boolean };
lower: { [k: string]: boolean };
zboost: { [k: string]: boolean };
status: { [k: string]: boolean };
volatileStatus: { [k: string]: boolean };
targets: { [k: string]: boolean };
skip: boolean;
multihit: boolean;
}
type Direction = 'less' | 'greater' | 'equal';
const MAX_PROCESSES = 1;
const RESULTS_MAX_LENGTH = 10;
const MAX_RANDOM_RESULTS = 30;
const dexesHelpMods = Object.keys((global.Dex?.dexes || {})).filter(x => x !== 'sourceMaps').join('</code>, <code>');
const supportedDexsearchRules: { [k: string]: string[] } = Object.assign(Object.create(null), {
movevalidation: ['stabmonsmovelegality', 'alphabetcupmovelegality'],
statmodification: ['350cupmod', 'flippedmod', 'scalemonsmod', 'badnboostedmod', 'reevolutionmod'],
banlist: [
'hoennpokedex', 'sinnohpokedex', 'oldunovapokedex', 'newunovapokedex', 'kalospokedex', 'oldalolapokedex',
'newalolapokedex', 'galarpokedex', 'isleofarmorpokedex', 'crowntundrapokedex', 'galarexpansionpokedex',
'paldeapokedex', 'kitakamipokedex', 'blueberrypokedex',
],
});
const dexsearchHelpRules = Object.values((supportedDexsearchRules)).flat().filter(x => x).join('</code>, <code>');
function toListString(arr: string[]) {
if (!arr.length) return '';
if (arr.length === 1) return arr[0];
if (arr.length === 2) return `${arr[0]} and ${arr[1]}`;
return `${arr.slice(0, -1).join(", ")}, and ${arr.slice(-1)[0]}`;
}
function checkCanAll(room: Room | null) {
if (!room) return false; // no, no good reason for using `all` in pms
const { isPersonal, isHelp } = room.settings;
// allowed if it's a groupchat
return !room.battle && !!isPersonal && !isHelp;
}
export const commands: Chat.ChatCommands = {
ds: 'dexsearch',
ds1: 'dexsearch',
ds2: 'dexsearch',
ds3: 'dexsearch',
ds4: 'dexsearch',
ds5: 'dexsearch',
ds6: 'dexsearch',
ds7: 'dexsearch',
ds8: 'dexsearch',
dsearch: 'dexsearch',
nds: 'dexsearch',
async dexsearch(target, room, user, connection, cmd, message) {
this.checkBroadcast();
if (!target) return this.parse('/help dexsearch');
if (target.length > 300) return this.errorReply('Dexsearch queries may not be longer than 300 characters.');
const targetGen = parseInt(cmd[cmd.length - 1]);
if (targetGen) target += `, mod=gen${targetGen}`;
const split = target.split(',').map(term => term.trim());
const index = split.findIndex(x => /^max\s*gen/i.test(x));
if (index >= 0) {
const genNum = parseInt(/\d*$/.exec(split[index])?.[0] || '');
if (!isNaN(genNum) && !(genNum < 1 || genNum > Dex.gen)) {
split[index] = `mod=gen${genNum}`;
target = split.join(',');
}
}
const defaultFormat = this.extractFormat(room?.settings.defaultFormat || room?.battle?.format);
if (!target.includes('mod=')) {
const dex = defaultFormat.dex;
if (dex) target += `, mod=${dex.currentMod}`;
}
if (cmd === 'nds' ||
(defaultFormat.format && Dex.formats.getRuleTable(defaultFormat.format).has('natdexmod'))) {
target += ', natdex';
}
const response = await runSearch({
target,
cmd: 'dexsearch',
canAll: !this.broadcastMessage || checkCanAll(room),
message: (this.broadcastMessage ? "" : message),
}, user);
if (!response.error && !this.runBroadcast()) return;
if (response.error) {
throw new Chat.ErrorMessage(response.error);
} else if (response.reply) {
this.sendReplyBox(response.reply);
} else if (response.dt) {
(Chat.commands.data as Chat.ChatHandler).call(
this, response.dt, room, user, connection, 'dt', this.broadcastMessage ? "" : message
);
}
},
dexsearchhelp() {
this.sendReply(
`|html| <details class="readmore"><summary><code>/dexsearch [parameter], [parameter], [parameter], ...</code>: searches for Pok\u00e9mon that fulfill the selected criteria<br/>` +
`Search categories are: type, tier, color, moves, ability, gen, resists, weak, recovery, zrecovery, priority, stat, weight, height, egg group, pivot.<br/>` +
`Valid colors are: green, red, blue, white, brown, yellow, purple, pink, gray and black.<br/>` +
`Valid tiers are: Uber/OU/UUBL/UU/RUBL/RU/NUBL/NU/PUBL/PU/ZUBL/ZU/NFE/LC/CAP/CAP NFE/CAP LC.<br/>` +
`Valid doubles tiers are: DUber/DOU/DBL/DUU/DNU.</summary>` +
`Types can be searched for by either having the type precede <code>type</code> or just using the type itself as a parameter; e.g., both <code>fire type</code> and <code>fire</code> show all Fire types; however, using <code>psychic</code> as a parameter will show all Pok\u00e9mon that learn the move Psychic and not Psychic types.<br/>` +
`<code>resists</code> followed by a type or move will show Pok\u00e9mon that resist that typing or move (e.g. <code>resists normal</code>).<br/>` +
`<code>weak</code> followed by a type or move will show Pok\u00e9mon that are weak to that typing or move (e.g. <code>weak fire</code>).<br/>` +
`<code>asc</code> or <code>desc</code> following a stat will show the Pok\u00e9mon in ascending or descending order of that stat respectively (e.g. <code>speed asc</code>).<br/>` +
`Inequality ranges use the characters <code>>=</code> for <code>≥</code> and <code><=</code> for <code>≤</code>; e.g., <code>hp <= 95</code> searches all Pok\u00e9mon with HP less than or equal to 95; <code>tier <= uu</code> searches all Pok\u00e9mon in singles tiers lower than UU.<br/>` +
`Parameters can be excluded through the use of <code>!</code>; e.g., <code>!water type</code> excludes all Water types.<br/>` +
`The parameter <code>mega</code> can be added to search for Mega Evolutions only, the parameter <code>gmax</code> can be added to search for Pok\u00e9mon capable of Gigantamaxing only, and the parameter <code>Fully Evolved</code> (or <code>FE</code>) can be added to search for fully-evolved Pok\u00e9mon.<br/>` +
`<code>Alola</code>, <code>Galar</code>, <code>Therian</code>, <code>Totem</code>, or <code>Primal</code> can be used as parameters to search for those formes.<br/>` +
`Parameters separated with <code>|</code> will be searched as alternatives for each other; e.g., <code>trick | switcheroo</code> searches for all Pok\u00e9mon that learn either Trick or Switcheroo.<br/>` +
`You can search for info in a specific generation by appending the generation to ds or by using the <code>maxgen</code> keyword; e.g. <code>/ds1 normal</code> or <code>/ds normal, maxgen1</code> searches for all Pok\u00e9mon that were Normal type in Generation I.<br/>` +
`You can search for info in a specific mod by using <code>mod=[mod name]</code>; e.g. <code>/nds mod=ssb, protean</code>. All valid mod names are: <code>${dexesHelpMods}</code><br/>` +
`You can search for info in a specific rule defined metagame by using <code>rule=[rule name]</code>; e.g. <code>/nds rule=alphabetcupmovelegality, v-create</code>. All supported rule names are: <code>${dexsearchHelpRules}</code><br/>` +
`By default, <code>/dexsearch</code> will search only Pok\u00e9mon obtainable in the current generation. Add the parameter <code>unreleased</code> to include unreleased Pok\u00e9mon. Add the parameter <code>natdex</code> (or use the command <code>/nds</code>) to include all past Pok\u00e9mon.<br/>` +
`Searching for a Pok\u00e9mon with both egg group and type parameters can be differentiated by adding the suffix <code>group</code> onto the egg group parameter; e.g., seaching for <code>grass, grass group</code> will show all Grass types in the Grass egg group.<br/>` +
`The parameter <code>monotype</code> will only show Pok\u00e9mon that are single-typed.<br/>` +
`The order of the parameters does not matter.<br/>`
);
},
rollmove: 'randommove',
randmove: 'randommove',
async randommove(target, room, user, connection, cmd, message) {
this.checkBroadcast(true);
target = target.slice(0, 300);
const targets = target.split(",");
const targetsBuffer = [];
let qty;
for (const arg of targets) {
if (!arg) continue;
const num = Number(arg);
if (Number.isInteger(num)) {
if (qty) throw new Chat.ErrorMessage("Only specify the number of Pok\u00e9mon Moves once.");
qty = num;
if (qty < 1 || MAX_RANDOM_RESULTS < qty) {
throw new Chat.ErrorMessage(`Number of random Pok\u00e9mon Moves must be between 1 and ${MAX_RANDOM_RESULTS}.`);
}
targetsBuffer.push(`random${qty}`);
} else {
targetsBuffer.push(arg);
}
}
if (!qty) targetsBuffer.push("random1");
const defaultFormat = this.extractFormat(room?.settings.defaultFormat || room?.battle?.format);
if (!target.includes('mod=')) {
const dex = defaultFormat.dex;
if (dex) targetsBuffer.push(`mod=${dex.currentMod}`);
}
const response = await runSearch({
target: targetsBuffer.join(","),
cmd: 'randmove',
canAll: !this.broadcastMessage || checkCanAll(room),
message: (this.broadcastMessage ? "" : message),
}, user);
if (!response.error && !this.runBroadcast(true)) return;
if (response.error) {
throw new Chat.ErrorMessage(response.error);
} else if (response.reply) {
this.sendReplyBox(response.reply);
} else if (response.dt) {
(Chat.commands.data as Chat.ChatHandler).call(
this, response.dt, room, user, connection, 'dt', this.broadcastMessage ? "" : message
);
}
},
randommovehelp: [
`/randommove - Generates random Pok\u00e9mon Moves based on given search conditions.`,
`/randommove uses the same parameters as /movesearch (see '/help ms').`,
`Adding a number as a parameter returns that many random Pok\u00e9mon Moves, e.g., '/randmove 6' returns 6 random Pok\u00e9mon Moves.`,
],
rollpokemon: 'randompokemon',
randpoke: 'randompokemon',
async randompokemon(target, room, user, connection, cmd, message) {
this.checkBroadcast(true);
target = target.slice(0, 300);
const targets = target.split(",");
const targetsBuffer = [];
let qty;
for (const arg of targets) {
if (!arg) continue;
const num = Number(arg);
if (Number.isInteger(num)) {
if (qty) throw new Chat.ErrorMessage("Only specify the number of Pok\u00e9mon once.");
qty = num;
if (qty < 1 || MAX_RANDOM_RESULTS < qty) {
throw new Chat.ErrorMessage(`Number of random Pok\u00e9mon must be between 1 and ${MAX_RANDOM_RESULTS}.`);
}
targetsBuffer.push(`random${qty}`);
} else {
targetsBuffer.push(arg);
}
}
if (!qty) targetsBuffer.push("random1");
const defaultFormat = this.extractFormat(room?.settings.defaultFormat || room?.battle?.format);
if (!target.includes('mod=')) {
const dex = defaultFormat.dex;
if (dex) targetsBuffer.push(`mod=${dex.currentMod}`);
}
const response = await runSearch({
target: targetsBuffer.join(","),
cmd: 'randpoke',
canAll: !this.broadcastMessage || checkCanAll(room),
message: (this.broadcastMessage ? "" : message),
}, user);
if (!response.error && !this.runBroadcast(true)) return;
if (response.error) {
throw new Chat.ErrorMessage(response.error);
} else if (response.reply) {
this.sendReplyBox(response.reply);
} else if (response.dt) {
(Chat.commands.data as Chat.ChatHandler).call(
this, response.dt, room, user, connection, 'dt', this.broadcastMessage ? "" : message
);
}
},
randompokemonhelp: [
`/randompokemon - Generates random Pok\u00e9mon based on given search conditions.`,
`/randompokemon uses the same parameters as /dexsearch (see '/help ds').`,
`Adding a number as a parameter returns that many random Pok\u00e9mon, e.g., '/randpoke 6' returns 6 random Pok\u00e9mon.`,
],
randability: 'randomability',
async randomability(target, room, user, connection, cmd, message) {
this.checkBroadcast(true);
target = target.slice(0, 300);
const targets = target.split(",");
const targetsBuffer = [];
let qty;
for (const arg of targets) {
if (!arg) continue;
const num = Number(arg);
if (Number.isInteger(num)) {
if (qty) throw new Chat.ErrorMessage("Only specify the number of abilities once.");
qty = num;
if (qty < 1 || MAX_RANDOM_RESULTS < qty) {
throw new Chat.ErrorMessage(`Number of random abilities must be between 1 and ${MAX_RANDOM_RESULTS}.`);
}
targetsBuffer.push(`random${qty}`);
} else {
targetsBuffer.push(arg);
}
}
if (!qty) targetsBuffer.push("random1");
const response = await runSearch({
target: targetsBuffer.join(","),
cmd: 'randability',
canAll: !this.broadcastMessage || checkCanAll(room),
message: (this.broadcastMessage ? "" : message),
});
if (!response.error && !this.runBroadcast(true)) return;
if (response.error) {
throw new Chat.ErrorMessage(response.error);
} else if (response.reply) {
this.sendReplyBox(response.reply);
} else if (response.dt) {
(Chat.commands.data as Chat.ChatHandler).call(
this, response.dt, room, user, connection, 'dt', this.broadcastMessage ? "" : message
);
}
},
randomabilityhelp: [
`/randability - Generates random Pok\u00e9mon ability based on given search conditions.`,
`/randability uses the same parameters as /abilitysearch (see '/help ds').`,
`Adding a number as a parameter returns that many random Pok\u00e9mon abilities, e.g., '/randabilitiy 6' returns 6 random abilities.`,
],
ms: 'movesearch',
ms1: 'movesearch',
ms2: 'movesearch',
ms3: 'movesearch',
ms4: 'movesearch',
ms5: 'movesearch',
ms6: 'movesearch',
ms7: 'movesearch',
ms8: 'movesearch',
msearch: 'movesearch',
nms: 'movesearch',
async movesearch(target, room, user, connection, cmd, message) {
this.checkBroadcast();
if (!target) return this.parse('/help movesearch');
target = target.slice(0, 300);
const targetGen = parseInt(cmd[cmd.length - 1]);
if (targetGen) target += `, mod=gen${targetGen}`;
const split = target.split(',').map(term => term.trim());
const index = split.findIndex(x => /^max\s*gen/i.test(x));
if (index >= 0) {
const genNum = parseInt(/\d*$/.exec(split[index])?.[0] || '');
if (!isNaN(genNum) && !(genNum < 1 || genNum > Dex.gen)) {
split[index] = `mod=gen${genNum}`;
target = split.join(',');
}
}
if (!target.includes('mod=')) {
const dex = this.extractFormat(room?.settings.defaultFormat || room?.battle?.format).dex;
if (dex) target += `, mod=${dex.currentMod}`;
}
if (cmd === 'nms') target += ', natdex';
const response = await runSearch({
target,
cmd: 'movesearch',
canAll: !this.broadcastMessage || checkCanAll(room),
message: (this.broadcastMessage ? "" : message),
}, user);
if (!response.error && !this.runBroadcast()) return;
if (response.error) {
throw new Chat.ErrorMessage(response.error);
} else if (response.reply) {
this.sendReplyBox(response.reply);
} else if (response.dt) {
(Chat.commands.data as Chat.ChatHandler).call(
this, response.dt, room, user, connection, 'dt', this.broadcastMessage ? "" : message
);
}
},
movesearchhelp() {
this.sendReplyBox(
`<code>/movesearch [parameter], [parameter], [parameter], ...</code>: searches for moves that fulfill the selected criteria.<br/><br/>` +
`Search categories are: type, category, gen, contest condition, flag, status inflicted, type boosted, Pok\u00e9mon targeted, and numeric range for base power, pp, priority, and accuracy.<br/><br/>` +
`<details class="readmore"><summary>Parameter Options</summary>` +
`- Types can be followed by <code> type</code> for clarity; e.g. <code>dragon type</code>.<br/>` +
`- Stat boosts must be preceded with <code>boosts </code>, and stat-lowering moves with <code>lowers </code>; e.g., <code>boosts attack</code> searches for moves that boost the Attack stat of either Pok\u00e9mon.<br/>` +
`- Z-stat boosts must be preceded with <code>zboosts </code>; e.g. <code>zboosts accuracy</code> searches for all Status moves with Z-Effects that boost the user's accuracy. Moves that have a Z-Effect of fully restoring the user's health can be searched for with <code>zrecovery</code>.<br/>` +
`- <code>zmove</code>, <code>max</code>, or <code>gmax</code> as parameters will search for Z-Moves, Max Moves, and G-Max Moves respectively.<br/>` +
`- Move targets must be preceded with <code>targets </code>; e.g. <code>targets user</code> searches for moves that target the user.<br/>` +
`- Valid move targets are: one ally, user or ally, one adjacent opponent, all Pokemon, all adjacent Pokemon, all adjacent opponents, user and allies, user's side, user's team, any Pokemon, opponent's side, one adjacent Pokemon, random adjacent Pokemon, scripted, and user.<br/>` +
`- Valid flags are: allyanim, bypasssub (bypasses Substitute), bite, bullet, cantusetwice, charge, contact, dance, defrost, distance (can target any Pokemon in Triples), failcopycat, failencore, failinstruct, failmefirst, failmimic, futuremove, gravity, heal, highcrit, instruct, metronome, mimic, mirror (reflected by Mirror Move), mustpressure, multihit, noassist, nonsky, noparentalbond, nosketch, nosleeptalk, ohko, pivot, pledgecombo, powder, priority, protect, pulse, punch, recharge, recovery, reflectable, secondary, slicing, snatch, sound, and wind.<br/>` +
`- <code>protection</code> as a parameter will search protection moves like Protect, Detect, etc.<br/>` +
`- A search that includes <code>!protect</code> will show all moves that bypass protection.<br/>` +
`</details><br/>` +
`<details class="readmore"><summary>Parameter Filters</summary>` +
`- Inequality ranges use the characters <code>></code> and <code><</code>.<br/>` +
`- Parameters can be excluded through the use of <code>!</code>; e.g. <code>!water type</code> excludes all Water-type moves.<br/>` +
`- <code>asc</code> or <code>desc</code> following a move property will arrange the names in ascending or descending order of that property, respectively; e.g., <code>basepower asc</code> will arrange moves in ascending order of their base powers.<br/>` +
`- Parameters separated with <code>|</code> will be searched as alternatives for each other; e.g. <code>fire | water</code> searches for all moves that are either Fire type or Water type.<br/>` +
`- If a Pok\u00e9mon is included as a parameter, only moves from its movepool will be included in the search.<br/>` +
`- You can search for info in a specific generation by appending the generation to ms; e.g. <code>/ms1 normal</code> searches for all moves that were Normal type in Generation I.<br/>` +
`- You can search for info in a specific mod by using <code>mod=[mod name]</code>; e.g. <code>/nms mod=ssb, dark, bp=100</code>. All valid mod names are: <code>${dexesHelpMods}</code><br/>` +
`- <code>/ms</code> will search all non-dexited moves (clickable in that game); you can include dexited moves by using <code>/nms</code> or by adding <code>natdex</code> as a parameter.<br/>` +
`- The order of the parameters does not matter.` +
`</details>`
);
},
isearch: 'itemsearch',
is: 'itemsearch',
is2: 'itemsearch',
is3: 'itemsearch',
is4: 'itemsearch',
is5: 'itemsearch',
is6: 'itemsearch',
is7: 'itemsearch',
is8: 'itemsearch',
async itemsearch(target, room, user, connection, cmd, message) {
this.checkBroadcast();
if (!target) return this.parse('/help itemsearch');
target = target.slice(0, 300);
const targetGen = parseInt(cmd[cmd.length - 1]);
if (targetGen) target = `maxgen${targetGen} ${target}`;
const response = await runSearch({
target,
cmd: 'itemsearch',
canAll: !this.broadcastMessage || checkCanAll(room),
message: (this.broadcastMessage ? "" : message),
}, user);
if (!response.error && !this.runBroadcast()) return;
if (response.error) {
throw new Chat.ErrorMessage(response.error);
} else if (response.reply) {
this.sendReplyBox(response.reply);
} else if (response.dt) {
(Chat.commands.data as Chat.ChatHandler).call(
this, response.dt, room, user, connection, 'dt', this.broadcastMessage ? "" : message
);
}
},
itemsearchhelp() {
this.sendReplyBox(
`<code>/itemsearch [item description]</code>: finds items that match the given keywords.<br/>` +
`This command accepts natural language. (tip: fewer words tend to work better)<br/>` +
`The <code>gen</code> keyword can be used to search for items introduced in a given generation; e.g., <code>/is gen4</code> searches for items introduced in Generation 4.<br/>` +
`To search for items within a generation, append the generation to <code>/is</code> or use the <code>maxgen</code> keyword; e.g., <code>/is4 Water-type</code> or <code>/is maxgen4 Water-type</code> searches for items whose Generation 4 description includes "Water-type".<br/>` +
`Searches with <code>fling</code> in them will find items with the specified Fling behavior.<br/>` +
`Searches with <code>natural gift</code> in them will find items with the specified Natural Gift behavior.`
);
},
randitem: 'randomitem',
async randomitem(target, room, user, connection, cmd, message) {
this.checkBroadcast(true);
target = target.slice(0, 300);
const targets = target.split(",");
const targetsBuffer = [];
let qty;
for (const arg of targets) {
if (!arg) continue;
const num = Number(arg);
if (Number.isInteger(num)) {
if (qty) throw new Chat.ErrorMessage("Only specify the number of items once.");
qty = num;
if (qty < 1 || MAX_RANDOM_RESULTS < qty) {
throw new Chat.ErrorMessage(`Number of random items must be between 1 and ${MAX_RANDOM_RESULTS}.`);
}
targetsBuffer.push(`random${qty}`);
} else {
targetsBuffer.push(arg);
}
}
if (!qty) targetsBuffer.push("random1");
const response = await runSearch({
target: targetsBuffer.join(","),
cmd: 'randitem',
canAll: !this.broadcastMessage || checkCanAll(room),
message: (this.broadcastMessage ? "" : message),
});
if (!response.error && !this.runBroadcast(true)) return;
if (response.error) {
throw new Chat.ErrorMessage(response.error);
} else if (response.reply) {
this.sendReplyBox(response.reply);
} else if (response.dt) {
(Chat.commands.data as Chat.ChatHandler).call(
this, response.dt, room, user, connection, 'dt', this.broadcastMessage ? "" : message
);
}
},
randomitemhelp: [
`/randitem - Generates random items based on given search conditions.`,
`/randitem uses the same parameters as /itemsearch (see '/help ds').`,
`Adding a number as a parameter returns that many random items, e.g., '/randitem 6' returns 6 random items.`,
],
asearch: 'abilitysearch',
as: 'abilitysearch',
as3: 'abilitysearch',
as4: 'abilitysearch',
as5: 'abilitysearch',
as6: 'abilitysearch',
as7: 'abilitysearch',
as8: 'abilitysearch',
async abilitysearch(target, room, user, connection, cmd, message) {
this.checkBroadcast();
if (!target) return this.parse('/help abilitysearch');
target = target.slice(0, 300);
const targetGen = parseInt(cmd[cmd.length - 1]);
if (targetGen) target += ` maxgen${targetGen}`;
const response = await runSearch({
target,
cmd: 'abilitysearch',
canAll: !this.broadcastMessage || checkCanAll(room),
message: (this.broadcastMessage ? "" : message),
}, user);
if (!response.error && !this.runBroadcast()) return;
if (response.error) {
throw new Chat.ErrorMessage(response.error);
} else if (response.reply) {
this.sendReplyBox(response.reply);
} else if (response.dt) {
(Chat.commands.data as Chat.ChatHandler).call(
this, response.dt, room, user, connection, 'dt', this.broadcastMessage ? "" : message
);
}
},
abilitysearchhelp() {
this.sendReplyBox(
`<code>/abilitysearch [ability description]</code>: finds abilities that match the given keywords.<br/>` +
`This command accepts natural language. (tip: fewer words tend to work better)<br/>` +
`The <code>gen</code> keyword can be used to search for abilities introduced in a given generation; e.g., <code>/as gen4</code> searches for abilities introduced in Generation 4.<br/>` +
`To search for abilities within a generation, append the generation to <code>/as</code> or use the <code>maxgen</code> keyword; e.g., <code>/as4 Water-type</code> or <code>/as maxgen4 Water-type</code> searches for abilities whose Generation 4 description includes "Water-type".`
);
},
learnset: 'learn',
learnall: 'learn',
learnlc: 'learn',
learn1: 'learn',
learn2: 'learn',
learn3: 'learn',
learn4: 'learn',
learn5: 'learn',
learn6: 'learn',
learn7: 'learn',
learn8: 'learn',
rbylearn: 'learn',
gsclearn: 'learn',
advlearn: 'learn',
dpplearn: 'learn',
bw2learn: 'learn',
oraslearn: 'learn',
usumlearn: 'learn',
sslearn: 'learn',
async learn(target, room, user, connection, cmd, message) {
if (!target) return this.parse('/help learn');
if (target.length > 300) throw new Chat.ErrorMessage(`Query too long.`);
const GENS: { [k: string]: number } = { rby: 1, gsc: 2, adv: 3, dpp: 4, bw2: 5, oras: 6, usum: 7, ss: 8 };
let cmdGen = GENS[cmd.slice(0, -5)];
if (cmdGen) target = `gen${cmdGen}, ${target}`;
cmdGen = Number(cmd.slice(5));
if (cmdGen) target = `gen${cmdGen}, ${target}`;
this.checkBroadcast();
const { format, dex, targets } = this.splitFormat(target);
const formatid = format ? format.id : dex.currentMod;
if (cmd === 'learnlc') targets.unshift('level5');
const response = await runSearch({
target: targets.join(','),
cmd: 'learn',
canAll: !this.broadcastMessage || checkCanAll(room),
message: formatid,
}, user);
if (!response.error && !this.runBroadcast()) return;
if (response.error) {
throw new Chat.ErrorMessage(response.error);
} else if (response.reply) {
this.sendReplyBox(response.reply);
}
},
learnhelp: [
`/learn [ruleset], [pokemon], [move, move, ...] - Displays how the Pok\u00e9mon can learn the given moves, if it can at all.`,
`!learn [ruleset], [pokemon], [move, move, ...] - Show everyone that information. Requires: + % @ # ~`,
`Specifying a ruleset is entirely optional. The ruleset can be a format, a generation (e.g.: gen3) or "min source gen [number]".`,
`A value of 'min source gen [number]' indicates that trading (or Pokémon Bank) from generations before [number] is not allowed.`,
`/learnlc displays how the Pok\u00e9mon can learn the given moves at level 5, if it can at all.`,
`/learnall displays all of the possible fathers for egg moves.`,
`A generation number can also be appended to /learn (e.g.: /learn4) to indicate which generation is used.`,
],
randtype: 'randomtype',
async randomtype(target, room, user, connection, cmd, message) {
this.checkBroadcast(true);
target = target.slice(0, 300);
const targets = target.split(",");
const targetsBuffer = [];
let qty;
for (const arg of targets) {
if (!arg) continue;
const num = Number(arg);
if (Number.isInteger(num)) {
if (qty) throw new Chat.ErrorMessage("Only specify the number of types once.");
qty = num;
if (qty < 1 || MAX_RANDOM_RESULTS < qty) {
throw new Chat.ErrorMessage(`Number of random types must be between 1 and ${MAX_RANDOM_RESULTS}.`);
}
targetsBuffer.push(`random${qty}`);
} else {
targetsBuffer.push(arg);
}
}
if (!qty) targetsBuffer.push("random1");
const response = await runSearch({
target: targetsBuffer.join(","),
cmd: 'randtype',
canAll: !this.broadcastMessage || checkCanAll(room),
message: (this.broadcastMessage ? "" : message),
});
if (!response.error && !this.runBroadcast(true)) return;
if (response.error) {
throw new Chat.ErrorMessage(response.error);
} else if (response.reply) {
this.sendReplyBox(response.reply);
} else if (response.dt) {
(Chat.commands.data as Chat.ChatHandler).call(
this, response.dt, room, user, connection, 'dt', this.broadcastMessage ? "" : message
);
}
},
randomtypehelp: [
`/randtype - Generates random types based on given search conditions.`,
`Adding a number as a parameter returns that many random items, e.g., '/randtype 6' returns 6 random types.`,
],
};
function getMod(target: string) {
const arr = target.split(',').map(x => x.trim());
const modTerm = arr.find(x => {
const sanitizedStr = x.toLowerCase().replace(/[^a-z0-9=]+/g, '');
return sanitizedStr.startsWith('mod=') && Dex.dexes[toID(sanitizedStr.split('=')[1])];
});
const count = arr.filter(x => {
const sanitizedStr = x.toLowerCase().replace(/[^a-z0-9=]+/g, '');
return sanitizedStr.startsWith('mod=');
}).length;
if (modTerm) arr.splice(arr.indexOf(modTerm), 1);
return { splitTarget: arr, usedMod: modTerm ? toID(modTerm.split(/ ?= ?/)[1]) : undefined, count };
}
function getRule(target: string) {
const arr = target.split(',').map(x => x.trim());
const ruleTerms: string[] = [];
for (const term of arr) {
const sanitizedStr = term.toLowerCase().replace(/[^a-z0-9=]+/g, '');
if (sanitizedStr.startsWith('rule=') && Dex.data.Rulesets[toID(sanitizedStr.split('=')[1])]) {
ruleTerms.push(term);
}
}
const count = arr.filter(x => {
const sanitizedStr = x.toLowerCase().replace(/[^a-z0-9=]+/g, '');
return sanitizedStr.startsWith('rule=');
}).length;
if (ruleTerms.length > 0) {
for (const rule of ruleTerms) {
arr.splice(arr.indexOf(rule), 1);
}
}
return { splitTarget: arr, usedRules: ruleTerms.map(
x => x.toLowerCase().replace(/[^a-z0-9=]+/g, '').split('rule=')[1]), count };
}
function prepareDexsearchValidator(usedMod: string | undefined, rules: FormatData[], nationalSearch: boolean | null) {
const format = Object.entries(Dex.data.Rulesets).find(([a, f]) => f.mod === usedMod)?.[1].name || 'gen9ou';
const ruleTable = Dex.formats.getRuleTable(Dex.formats.get(format));
const additionalRules = [];
for (const rule of rules) {
if (!ruleTable.has(toID(rule.name))) additionalRules.push(toID(rule.name));
}
if (nationalSearch && !ruleTable.has('natdexmod')) additionalRules.push('natdexmod');
if (nationalSearch && ruleTable.valueRules.has('minsourcegen')) additionalRules.push('!!minsourcegen=3');
return TeamValidator.get(`${format}${additionalRules.length ? `@@@${additionalRules.join(',')}` : ''}`);
}
function runDexsearch(target: string, cmd: string, canAll: boolean, message: string, isTest: boolean) {
const searches: DexOrGroup[] = [];
const { splitTarget: remainingTargets, usedMod, count: modCount } = getMod(target);
const { splitTarget, usedRules } = getRule(remainingTargets.join(','));
if (modCount > 1) {
return { error: `You can't run searches for multiple mods.` };
}
for (const str of splitTarget) {
const sanitizedStr = str.toLowerCase().replace(/[^a-z0-9=]+/g, '');
if (sanitizedStr.startsWith('mod=') || sanitizedStr.startsWith('rule=')) {
return { error: `${sanitizedStr.split('=')[1]} is an invalid mod or rule, see /dexsearchhelp.` };
}
}
const mod = Dex.mod(usedMod || 'base');
const rules: FormatData[] = [];
for (const rule of usedRules) {
if (!dexsearchHelpRules.includes(rule))
return { error: `${rule} is an unsupported rule, see /dexsearchhelp` };
rules.push(Dex.data.Rulesets[rule]);
}
const allTiers: { [k: string]: TierTypes.Singles | TierTypes.Other } = Object.assign(Object.create(null), {
anythinggoes: 'AG', ag: 'AG',
uber: 'Uber', ubers: 'Uber', ou: 'OU',
uubl: 'UUBL', uu: 'UU',
rubl: 'RUBL', ru: 'RU',
nubl: 'NUBL', nu: 'NU',
publ: 'PUBL', pu: 'PU',
zubl: 'ZUBL', zu: 'ZU',
nfe: 'NFE',
lc: 'LC',
cap: 'CAP', caplc: 'CAP LC', capnfe: 'CAP NFE',
});
const singlesTiersValues: { [k: string]: number } = Object.assign(Object.create(null), {
AG: 14, Uber: 13,
OU: 12, CAP: 12,
UUBL: 11, UU: 10,
RUBL: 9, RU: 8,
NUBL: 7, NU: 6,
PUBL: 5, PU: 4,
ZUBL: 3, ZU: 2,
NFE: 1, 'CAP NFE': 1,
LC: 0, 'CAP LC': 0,
});
const allDoublesTiers: { [k: string]: TierTypes.Singles | TierTypes.Other } = Object.assign(Object.create(null), {
doublesubers: 'DUber', doublesuber: 'DUber', duber: 'DUber', dubers: 'DUber',
doublesou: 'DOU', dou: 'DOU',
doublesbl: 'DBL', dbl: 'DBL',
doublesuu: 'DUU', duu: 'DUU',
doublesnu: '(DUU)', dnu: '(DUU)',
});
const doublesTiersValues: { [k: string]: number } = Object.assign(Object.create(null), {
DUber: 4, DOU: 3,
DBL: 2, DUU: 1,
'(DUU)': 0,
});
const allTypes = Object.create(null);
for (const type of mod.types.all()) {
allTypes[type.id] = type.name;
}
const allColors = ['green', 'red', 'blue', 'white', 'brown', 'yellow', 'purple', 'pink', 'gray', 'black'];
const allEggGroups: { [k: string]: string } = Object.assign(Object.create(null), {
amorphous: 'Amorphous',
bug: 'Bug',
ditto: 'Ditto',
dragon: 'Dragon',
fairy: 'Fairy',
field: 'Field',
flying: 'Flying',
grass: 'Grass',
humanlike: 'Human-Like',
mineral: 'Mineral',
monster: 'Monster',
undiscovered: 'Undiscovered',
water1: 'Water 1',
water2: 'Water 2',
water3: 'Water 3',
});
const allFormes = ['alola', 'galar', 'hisui', 'paldea', 'primal', 'therian', 'totem'];
const allStats = ['hp', 'atk', 'def', 'spa', 'spd', 'spe', 'bst', 'weight', 'height', 'gen'];
const allStatAliases: { [k: string]: string } = {
attack: 'atk', defense: 'def', specialattack: 'spa', spc: 'spa', special: 'spa', spatk: 'spa',
specialdefense: 'spd', spdef: 'spd', speed: 'spe', wt: 'weight', ht: 'height', generation: 'gen',
};
let showAll = false;
let sort = null;
let megaSearch = null;
let gmaxSearch = null;
let tierSearch = null;
let capSearch: boolean | null = null;
let nationalSearch = null;
let unreleasedSearch = null;
let fullyEvolvedSearch = null;
let restrictedSearch = null;
let singleTypeSearch = null;
let randomOutput = 0;
let tierInequalitySearch = false;
const validParameter = (cat: string, param: string, isNotSearch: boolean, input: string) => {
const uniqueTraits = ['colors', 'gens'];
const tierTraits = ['tiers', 'doubles tiers'];
for (const group of searches) {
const g = group[cat as keyof DexOrGroup];
if (g === undefined) continue;
if (tierTraits.includes(cat) && tierInequalitySearch) continue;
if (typeof g !== 'boolean' && g[param] === undefined) {
if (uniqueTraits.includes(cat)) {
for (const currentParam in g) {
if (g[currentParam] !== isNotSearch && !isNotSearch) return `A Pokémon cannot have multiple ${cat}.`;
}
}
continue;
}
if (typeof g !== 'boolean' && g[param] === isNotSearch) {
return `A search cannot both include and exclude '${input}'.`;
} else {
return `The search included '${(isNotSearch ? "!" : "") + input}' more than once.`;
}
}
return false;
};
for (const andGroup of splitTarget) {
const orGroup: DexOrGroup = {
abilities: {}, tiers: {}, doublesTiers: {}, colors: {}, 'egg groups': {}, formes: {},
gens: {}, moves: {}, types: {}, resists: {}, weak: {}, stats: {}, skip: false,
};
const parameters = andGroup.split("|");
if (parameters.length > 3) return { error: "No more than 3 alternatives for each parameter may be used." };
for (const parameter of parameters) {
let isNotSearch = false;
target = parameter.trim().toLowerCase();
if (target.startsWith('!')) {
isNotSearch = true;
target = target.substr(1);
}
let isTierInequalityParam = false;
const tierInequality: boolean[] = [];
if (target.startsWith('tier')) {
if (isNotSearch) return { error: "You cannot use the negation symbol '!' with inequality tier searches." };
target = target.substr(4).trim();
if (!target.startsWith('>') && !target.startsWith('<')) {
return { error: "You must use an inequality operator '>' or '<' with performing tier inequality searchs." };
}
isTierInequalityParam = true;
tierInequalitySearch = true;
tierInequality[0] = target.startsWith('>');
target = target.substr(1).trim();
tierInequality[1] = target.startsWith('=');
if (tierInequality[1]) target = target.substr(1).trim();
}
const targetAbility = mod.abilities.get(target);
if (targetAbility.exists) {
const invalid = validParameter("abilities", targetAbility.id, isNotSearch, targetAbility.name);
if (invalid) return { error: invalid };
orGroup.abilities[targetAbility.name] = !isNotSearch;
continue;
}
if (toID(target) in allTiers) {
target = allTiers[toID(target)];
if (target.startsWith("CAP")) {
if (capSearch === isNotSearch) return { error: "A search cannot both include and exclude CAP tiers." };
capSearch = !isNotSearch;
}
const invalid = validParameter("tiers", target, isNotSearch, target);
if (invalid) return { error: invalid };
tierSearch = tierSearch || !isNotSearch;
if (isTierInequalityParam) {
const tierValue = singlesTiersValues[target];
const entires = Object.entries(singlesTiersValues);
for (const [key, value] of entires) {
const useTier = (value > tierValue && tierInequality[0]) || (value < tierValue && !tierInequality[0]);
if (useTier && (!key.startsWith('CAP') || capSearch)) {
orGroup.tiers[key] = true;
} else if (tierValue === value && tierInequality[1]) {
orGroup.tiers[key] = true;
}
}
} else {
orGroup.tiers[target] = !isNotSearch;
}
continue;
}
if (toID(target) in allDoublesTiers) {
target = allDoublesTiers[toID(target)];
const invalid = validParameter("doubles tiers", target, isNotSearch, target);
if (invalid) return { error: invalid };
tierSearch = tierSearch || !isNotSearch;
if (isTierInequalityParam) {
const tierValue = doublesTiersValues[target];
const entires = Object.entries(doublesTiersValues);
for (const [key, value] of entires) {
if ((value > tierValue && tierInequality[0]) || (value < tierValue && !tierInequality[0])) {
orGroup.doublesTiers[key] = true;
} else if (tierValue === value && tierInequality[1]) {
orGroup.doublesTiers[key] = true;
}
}
} else {
orGroup.doublesTiers[target] = !isNotSearch;
}
continue;
}
if (allColors.includes(target)) {
target = target.charAt(0).toUpperCase() + target.slice(1);
const invalid = validParameter("colors", target, isNotSearch, target);
if (invalid) return { error: invalid };
orGroup.colors[target] = !isNotSearch;
continue;
}
const targetMove = mod.moves.get(target);
if (targetMove.exists) {
const invalid = validParameter("moves", targetMove.id, isNotSearch, target);
if (invalid) return { error: invalid };
orGroup.moves[targetMove.id] = !isNotSearch;
continue;
}
let targetType;
if (target.endsWith('type')) {
targetType = toID(target.substring(0, target.indexOf('type')));
} else {
targetType = toID(target);
}
if (targetType in allTypes) {
target = allTypes[targetType];
const invalid = validParameter("types", target, isNotSearch, target);
if (invalid) return { error: invalid };
if ((orGroup.types[target] && isNotSearch) || (orGroup.types[target] === false && !isNotSearch)) {
return { error: 'A search cannot both exclude and include a type.' };
}
orGroup.types[target] = !isNotSearch;
continue;
}
if (['mono', 'monotype'].includes(toID(target))) {
if (singleTypeSearch === isNotSearch) return { error: "A search cannot include and exclude 'monotype'." };
if (parameters.length > 1) return { error: "The parameter 'monotype' cannot have alternative parameters." };
singleTypeSearch = !isNotSearch;
orGroup.skip = true;
continue;
}
if (target === 'natdex') {
if (parameters.length > 1) return { error: "The parameter 'natdex' cannot have alternative parameters." };
nationalSearch = true;
orGroup.skip = true;
continue;
}
if (target === 'unreleased') {
if (parameters.length > 1) return { error: "The parameter 'unreleased' cannot have alternative parameters." };
unreleasedSearch = true;
orGroup.skip = true;
continue;
}
let groupIndex = target.indexOf('group');
if (groupIndex === -1) groupIndex = target.length;
if (groupIndex !== target.length || toID(target) in allEggGroups) {
target = toID(target.substring(0, groupIndex));
if (target in allEggGroups) {
target = allEggGroups[toID(target)];
const invalid = validParameter("egg groups", target, isNotSearch, target);
if (invalid) return { error: invalid };
orGroup['egg groups'][target] = !isNotSearch;
continue;
} else {
return { error: `'${target}' is not a recognized egg group.` };
}
}
if (toID(target) in allEggGroups) {
target = allEggGroups[toID(target)];
const invalid = validParameter("egg groups", target, isNotSearch, target);
if (invalid) return { error: invalid };
orGroup['egg groups'][target] = !isNotSearch;
continue;
}
let targetInt = 0;
if (target.substr(0, 1) === 'g' && Number.isInteger(parseFloat(target.substr(1)))) {
targetInt = parseInt(target.substr(1).trim());
} else if (target.substr(0, 3) === 'gen' && Number.isInteger(parseFloat(target.substr(3)))) {
targetInt = parseInt(target.substr(3).trim());
}
if (0 < targetInt && targetInt <= mod.gen) {
const invalid = validParameter("gens", String(targetInt), isNotSearch, target);
if (invalid) return { error: invalid };
orGroup.gens[targetInt] = !isNotSearch;
continue;
}
if (target.endsWith(' asc') || target.endsWith(' desc')) {
if (parameters.length > 1) {
return { error: `The parameter '${target.split(' ')[1]}' cannot have alternative parameters.` };
}
const stat = allStatAliases[toID(target.split(' ')[0])] || toID(target.split(' ')[0]);
if (!allStats.includes(stat)) return { error: `'${target}' did not contain a valid stat.` };
sort = `${stat}${target.endsWith(' asc') ? '+' : '-'}`;
orGroup.skip = true;
break;
}
if (target === 'all') {
if (!canAll) return { error: "A search with the parameter 'all' cannot be broadcast." };
if (parameters.length > 1) return { error: "The parameter 'all' cannot have alternative parameters." };
showAll = true;
orGroup.skip = true;
break;
}
if (target.substr(0, 6) === 'random' && cmd === 'randpoke') {
// Validation for this is in the /randpoke command
randomOutput = parseInt(target.substr(6));
orGroup.skip = true;
continue;
}
if (allFormes.includes(toID(target))) {
target = toID(target);
orGroup.formes[target] = !isNotSearch;
continue;
}
if (target === 'megas' || target === 'mega') {
if (megaSearch === isNotSearch) return { error: "A search cannot include and exclude 'mega'." };
if (parameters.length > 1) return { error: "The parameter 'mega' cannot have alternative parameters." };
megaSearch = !isNotSearch;
orGroup.skip = true;