9
9
*/
10
10
11
11
import { ProcessManager , Utils } from '../../lib' ;
12
+ import type { FormatData } from '../../sim/dex-formats' ;
12
13
import { TeamValidator } from '../../sim/team-validator' ;
13
14
import { Chat } from '../chat' ;
14
15
@@ -52,7 +53,17 @@ type Direction = 'less' | 'greater' | 'equal';
52
53
const MAX_PROCESSES = 1 ;
53
54
const RESULTS_MAX_LENGTH = 10 ;
54
55
const MAX_RANDOM_RESULTS = 30 ;
55
- const dexesHelp = Object . keys ( ( global . Dex ?. dexes || { } ) ) . filter ( x => x !== 'sourceMaps' ) . join ( '</code>, <code>' ) ;
56
+ const dexesHelpMods = Object . keys ( ( global . Dex ?. dexes || { } ) ) . filter ( x => x !== 'sourceMaps' ) . join ( '</code>, <code>' ) ;
57
+ const supportedDexsearchRules : { [ k : string ] : string [ ] } = Object . assign ( Object . create ( null ) , {
58
+ movevalidation : [ 'stabmonsmovelegality' , 'alphabetcupmovelegality' ] ,
59
+ statmodification : [ '350cupmod' , 'flippedmod' , 'scalemonsmod' , 'badnboostedmod' , 'reevolutionmod' ] ,
60
+ banlist : [
61
+ 'hoennpokedex' , 'sinnohpokedex' , 'oldunovapokedex' , 'newunovapokedex' , 'kalospokedex' , 'oldalolapokedex' ,
62
+ 'newalolapokedex' , 'galarpokedex' , 'isleofarmorpokedex' , 'crowntundrapokedex' , 'galarexpansionpokedex' ,
63
+ 'paldeapokedex' , 'kitakamipokedex' , 'blueberrypokedex' ,
64
+ ] ,
65
+ } ) ;
66
+ const dexsearchHelpRules = Object . values ( ( supportedDexsearchRules ) ) . flat ( ) . filter ( x => x ) . join ( '</code>, <code>' ) ;
56
67
57
68
function toListString ( arr : string [ ] ) {
58
69
if ( ! arr . length ) return '' ;
@@ -138,7 +149,8 @@ export const commands: Chat.ChatCommands = {
138
149
`<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/>` +
139
150
`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/>` +
140
151
`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/>` +
141
- `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>${ dexesHelp } </code><br />` +
152
+ `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/>` +
153
+ `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/>` +
142
154
`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/>` +
143
155
`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/>` +
144
156
`The parameter <code>monotype</code> will only show Pok\u00e9mon that are single-typed.<br/>` +
@@ -363,7 +375,7 @@ export const commands: Chat.ChatCommands = {
363
375
`- 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/>` +
364
376
`- If a Pok\u00e9mon is included as a parameter, only moves from its movepool will be included in the search.<br/>` +
365
377
`- 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/>` +
366
- `- 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>${ dexesHelp } </code><br />` +
378
+ `- 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/>` +
367
379
`- <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/>` +
368
380
`- The order of the parameters does not matter.` +
369
381
`</details>`
@@ -618,14 +630,61 @@ function getMod(target: string) {
618
630
return { splitTarget : arr , usedMod : modTerm ? toID ( modTerm . split ( / ? = ? / ) [ 1 ] ) : undefined , count } ;
619
631
}
620
632
633
+ function getRule ( target : string ) {
634
+ const arr = target . split ( ',' ) . map ( x => x . trim ( ) ) ;
635
+ const ruleTerms : string [ ] = [ ] ;
636
+ for ( const term of arr ) {
637
+ const sanitizedStr = term . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 = ] + / g, '' ) ;
638
+ if ( sanitizedStr . startsWith ( 'rule=' ) && Dex . data . Rulesets [ toID ( sanitizedStr . split ( '=' ) [ 1 ] ) ] ) {
639
+ ruleTerms . push ( term ) ;
640
+ }
641
+ }
642
+ const count = arr . filter ( x => {
643
+ const sanitizedStr = x . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 = ] + / g, '' ) ;
644
+ return sanitizedStr . startsWith ( 'rule=' ) ;
645
+ } ) . length ;
646
+ if ( ruleTerms . length > 0 ) {
647
+ for ( const rule of ruleTerms ) {
648
+ arr . splice ( arr . indexOf ( rule ) , 1 ) ;
649
+ }
650
+ }
651
+ return { splitTarget : arr , usedRules : ruleTerms . map (
652
+ x => x . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 = ] + / g, '' ) . split ( 'rule=' ) [ 1 ] ) , count } ;
653
+ }
654
+
655
+ function prepareDexsearchValidator ( usedMod : string | undefined , rules : FormatData [ ] , nationalSearch : boolean | null ) {
656
+ const format = Object . entries ( Dex . data . Rulesets ) . find ( ( [ a , f ] ) => f . mod === usedMod ) ?. [ 1 ] . name || 'gen9ou' ;
657
+ const ruleTable = Dex . formats . getRuleTable ( Dex . formats . get ( format ) ) ;
658
+ const additionalRules = [ ] ;
659
+ for ( const rule of rules ) {
660
+ if ( ! ruleTable . has ( toID ( rule . name ) ) ) additionalRules . push ( toID ( rule . name ) ) ;
661
+ }
662
+ if ( nationalSearch && ! ruleTable . has ( 'natdexmod' ) ) additionalRules . push ( 'natdexmod' ) ;
663
+ if ( nationalSearch && ruleTable . valueRules . has ( 'minsourcegen' ) ) additionalRules . push ( '!!minsourcegen=3' ) ;
664
+ return TeamValidator . get ( `${ format } ${ additionalRules . length ? `@@@${ additionalRules . join ( ',' ) } ` : '' } ` ) ;
665
+ }
666
+
621
667
function runDexsearch ( target : string , cmd : string , canAll : boolean , message : string , isTest : boolean ) {
622
668
const searches : DexOrGroup [ ] = [ ] ;
623
- const { splitTarget, usedMod, count : c } = getMod ( target ) ;
624
- if ( c > 1 ) {
669
+ const { splitTarget : remainingTargets , usedMod, count : modCount } = getMod ( target ) ;
670
+ const { splitTarget, usedRules } = getRule ( remainingTargets . join ( ',' ) ) ;
671
+ if ( modCount > 1 ) {
625
672
return { error : `You can't run searches for multiple mods.` } ;
626
673
}
627
-
674
+ for ( const str of splitTarget ) {
675
+ const sanitizedStr = str . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 = ] + / g, '' ) ;
676
+ if ( sanitizedStr . startsWith ( 'mod=' ) || sanitizedStr . startsWith ( 'rule=' ) ) {
677
+ return { error : `${ sanitizedStr . split ( '=' ) [ 1 ] } is an invalid mod or rule, see /dexsearchhelp.` } ;
678
+ }
679
+ }
628
680
const mod = Dex . mod ( usedMod || 'base' ) ;
681
+ const rules : FormatData [ ] = [ ] ;
682
+ for ( const rule of usedRules ) {
683
+ if ( ! dexsearchHelpRules . includes ( rule ) )
684
+ return { error : `${ rule } is an unsupported rule, see /dexsearchhelp` } ;
685
+ rules . push ( Dex . data . Rulesets [ rule ] ) ;
686
+ }
687
+
629
688
const allTiers : { [ k : string ] : TierTypes . Singles | TierTypes . Other } = Object . assign ( Object . create ( null ) , {
630
689
anythinggoes : 'AG' , ag : 'AG' ,
631
690
uber : 'Uber' , ubers : 'Uber' , ou : 'OU' ,
@@ -1146,13 +1205,38 @@ function runDexsearch(target: string, cmd: string, canAll: boolean, message: str
1146
1205
} ;
1147
1206
}
1148
1207
1208
+ // Prepare move validator and pokemonSource outside the hot loop
1209
+ // but don't prepare them at all if there are no moves to check...
1210
+ // These only ever get accessed if there are moves or banlists to filter by.
1211
+ let validator ;
1212
+ let pokemonSource ;
1213
+ if ( Object . values ( searches ) . some ( search => ! ! Object . keys ( search . moves ) . length ) ) {
1214
+ validator = prepareDexsearchValidator ( usedMod , rules , nationalSearch ) ;
1215
+ }
1216
+
1149
1217
const dex : { [ k : string ] : Species } = { } ;
1150
1218
for ( const species of mod . species . all ( ) ) {
1151
1219
const megaSearchResult = megaSearch === null || megaSearch === ! ! species . isMega ;
1152
1220
const gmaxSearchResult = gmaxSearch === null || gmaxSearch === species . name . endsWith ( '-Gmax' ) ;
1153
1221
const fullyEvolvedSearchResult = fullyEvolvedSearch === null || fullyEvolvedSearch !== species . nfe ;
1154
1222
const restrictedSearchResult = restrictedSearch === null ||
1155
1223
restrictedSearch === species . tags . includes ( 'Restricted Legendary' ) ;
1224
+
1225
+ /**
1226
+ * Not every ruleset with an onValidateSet function is specifically to exclude mons.
1227
+ * In the current list of supported rules only the Pokedex rules do such which is
1228
+ * why this step is ignored for other rules. Rules can be added for this functionality
1229
+ * in the supportedDexSearchTypes mapping at the top of the function.
1230
+ */
1231
+ let ruleResult = true ;
1232
+ for ( const rule of rules ) {
1233
+ if ( ! ruleResult ) break ;
1234
+ if ( ! supportedDexsearchRules [ 'banlist' ] . includes ( toID ( rule . name ) ) ) continue ;
1235
+ if ( ! validator ) validator = prepareDexsearchValidator ( usedMod , rules , nationalSearch ) ;
1236
+ ruleResult = ! rule . onValidateSet ?. call ( validator ,
1237
+ { name : species . name , species : species . id } as PokemonSet , validator . format , { } , { } ) ;
1238
+ }
1239
+
1156
1240
if (
1157
1241
species . gen <= mod . gen &&
1158
1242
(
@@ -1163,9 +1247,15 @@ function runDexsearch(target: string, cmd: string, canAll: boolean, message: str
1163
1247
megaSearchResult &&
1164
1248
gmaxSearchResult &&
1165
1249
fullyEvolvedSearchResult &&
1166
- restrictedSearchResult
1250
+ restrictedSearchResult &&
1251
+ ruleResult
1167
1252
) {
1168
- dex [ species . id ] = species ;
1253
+ let newSpecies = species ;
1254
+ for ( const rule of rules ) {
1255
+ newSpecies = rule ?. onModifySpecies ?. call ( { dex : mod , clampIntRange : Utils . clampIntRange , toID } as Battle ,
1256
+ newSpecies ) || newSpecies ;
1257
+ }
1258
+ dex [ newSpecies . id ] = newSpecies ;
1169
1259
}
1170
1260
}
1171
1261
@@ -1176,19 +1266,6 @@ function runDexsearch(target: string, cmd: string, canAll: boolean, message: str
1176
1266
Object . values ( search ) . reduce ( accumulateKeyCount , 0 )
1177
1267
) ) ;
1178
1268
1179
- // Prepare move validator and pokemonSource outside the hot loop
1180
- // but don't prepare them at all if there are no moves to check...
1181
- // These only ever get accessed if there are moves to filter by.
1182
- let validator ;
1183
- let pokemonSource ;
1184
- if ( Object . values ( searches ) . some ( search => Object . keys ( search . moves ) . length !== 0 ) ) {
1185
- const format = Object . entries ( Dex . data . Rulesets ) . find ( ( [ a , f ] ) => f . mod === usedMod ) ?. [ 1 ] . name || 'gen9ou' ;
1186
- const ruleTable = Dex . formats . getRuleTable ( Dex . formats . get ( format ) ) ;
1187
- const additionalRules = [ ] ;
1188
- if ( nationalSearch && ! ruleTable . has ( 'natdexmod' ) ) additionalRules . push ( 'natdexmod' ) ;
1189
- if ( nationalSearch && ruleTable . valueRules . has ( 'minsourcegen' ) ) additionalRules . push ( '!!minsourcegen=3' ) ;
1190
- validator = TeamValidator . get ( `${ format } ${ additionalRules . length ? `@@@${ additionalRules . join ( ',' ) } ` : '' } ` ) ;
1191
- }
1192
1269
for ( const alts of searches ) {
1193
1270
if ( alts . skip ) continue ;
1194
1271
const altsMoves = Object . keys ( alts . moves ) . map ( x => mod . moves . get ( x ) ) . filter ( move => move . gen <= mod . gen ) ;
@@ -1352,13 +1429,29 @@ function runDexsearch(target: string, cmd: string, canAll: boolean, message: str
1352
1429
}
1353
1430
if ( matched ) continue ;
1354
1431
1355
- for ( const move of altsMoves ) {
1356
- pokemonSource = validator ?. allSources ( ) ;
1357
- if ( validator && ! validator . checkCanLearn ( move , dex [ mon ] , pokemonSource ) === alts . moves [ move . id ] ) {
1358
- matched = true ;
1359
- break ;
1432
+ if ( validator ) {
1433
+ for ( const move of altsMoves ) {
1434
+ pokemonSource = validator . allSources ( ) ;
1435
+ const isNotSearch = ! alts . moves [ move . id ] ;
1436
+
1437
+ let matchRule = false ;
1438
+ let numMoveValidationRules = 0 ;
1439
+ for ( const rule of rules ) {
1440
+ if ( ! supportedDexsearchRules [ 'movevalidation' ] . includes ( toID ( rule . name ) ) ) continue ;
1441
+ else numMoveValidationRules ++ ;
1442
+ matchRule = ! rule . checkCanLearn ?. call (
1443
+ validator , move , dex [ mon ] , pokemonSource , { } as PokemonSet ) === ! isNotSearch ;
1444
+ if ( matchRule === ! isNotSearch ) break ;
1445
+ }
1446
+ const matchNormally = ! validator . checkCanLearn ( move , dex [ mon ] , pokemonSource ) === ! isNotSearch ;
1447
+
1448
+ if ( ( ! isNotSearch && ( matchNormally || ( numMoveValidationRules > 0 && matchRule ) ) ) ||
1449
+ ( isNotSearch && matchNormally && ( numMoveValidationRules === 0 || matchRule ) ) ) {
1450
+ matched = true ;
1451
+ break ;
1452
+ }
1453
+ if ( pokemonSource && ! pokemonSource . size ( ) ) break ;
1360
1454
}
1361
- if ( pokemonSource && ! pokemonSource . size ( ) ) break ;
1362
1455
}
1363
1456
if ( matched ) continue ;
1364
1457
@@ -1368,51 +1461,49 @@ function runDexsearch(target: string, cmd: string, canAll: boolean, message: str
1368
1461
1369
1462
const stat = sort ?. slice ( 0 , - 1 ) ;
1370
1463
1371
- function getSortValue ( name : string ) {
1372
- if ( ! stat ) return 0 ;
1373
- const mon = mod . species . get ( name ) ;
1374
- if ( stat === 'bst' ) {
1375
- return mon . bst ;
1464
+ function getSortValue ( species : Species ) {
1465
+ if ( ! stat ) {
1466
+ return 0 ;
1467
+ } else if ( stat === 'bst' ) {
1468
+ return species . bst ;
1376
1469
} else if ( stat === 'weight' ) {
1377
- return mon . weighthg ;
1470
+ return species . weighthg ;
1378
1471
} else if ( stat === 'height' ) {
1379
- return mon . heightm ;
1472
+ return species . heightm ;
1380
1473
} else if ( stat === 'gen' ) {
1381
- return mon . gen ;
1474
+ return species . gen ;
1382
1475
} else {
1383
- return mon . baseStats [ stat as StatID ] ;
1476
+ return species . baseStats [ stat as StatID ] ;
1384
1477
}
1385
1478
}
1386
1479
1387
- let results : string [ ] = [ ] ;
1388
- for ( const mon of Object . keys ( dex ) . sort ( ) ) {
1389
- if ( singleTypeSearch !== null && ( dex [ mon ] . types . length === 1 ) !== singleTypeSearch ) continue ;
1390
- const isRegionalForm = ( [ "Alola" , "Galar" , "Hisui" ] . includes ( dex [ mon ] . forme ) || dex [ mon ] . forme . startsWith ( "Paldea" ) ) &&
1391
- dex [ mon ] . baseSpecies !== "Pikachu" ;
1392
- const maskForm = dex [ mon ] . baseSpecies === "Ogerpon" && ! dex [ mon ] . forme . endsWith ( "Tera" ) ;
1480
+ let results : Species [ ] = [ ] ;
1481
+ for ( const mon of Object . values ( dex ) . sort ( ) ) {
1482
+ if ( singleTypeSearch !== null && ( mon . types . length === 1 ) !== singleTypeSearch ) continue ;
1483
+ const isRegionalForm = ( [ "Alola" , "Galar" , "Hisui" ] . includes ( mon . forme ) || mon . forme . startsWith ( "Paldea" ) ) &&
1484
+ mon . baseSpecies !== "Pikachu" ;
1485
+ const maskForm = mon . baseSpecies === "Ogerpon" && ! mon . forme . endsWith ( "Tera" ) ;
1393
1486
const allowGmax = ( gmaxSearch || tierSearch ) ;
1394
- if ( ! isRegionalForm && ! maskForm && dex [ mon ] . baseSpecies && results . includes ( dex [ mon ] . baseSpecies ) &&
1395
- getSortValue ( mon ) === getSortValue ( dex [ mon ] . baseSpecies ) ) continue ;
1396
- const teraFormeChangesFrom = dex [ mon ] . forme . endsWith ( "Tera" ) ? ! Array . isArray ( dex [ mon ] . battleOnly ) ?
1397
- dex [ mon ] . battleOnly ! : null : null ;
1398
- if ( teraFormeChangesFrom && results . includes ( teraFormeChangesFrom ) &&
1399
- getSortValue ( mon ) === getSortValue ( teraFormeChangesFrom ) ) continue ;
1400
- if ( dex [ mon ] . isNonstandard === 'Gigantamax' && ! allowGmax ) continue ;
1401
- results . push ( dex [ mon ] . name ) ;
1487
+ if ( ! isRegionalForm && ! maskForm && mon . baseSpecies && results . includes ( mod . species . get ( mon . baseSpecies ) ) &&
1488
+ getSortValue ( mon ) === getSortValue ( mod . species . get ( mon . baseSpecies ) ) ) continue ;
1489
+ const teraFormeChangesFrom = mon . forme . endsWith ( "Tera" ) ? ! Array . isArray ( mon . battleOnly ) ?
1490
+ mon . battleOnly ! : null : null ;
1491
+ if ( teraFormeChangesFrom && results . includes ( mod . species . get ( teraFormeChangesFrom ) ) &&
1492
+ getSortValue ( mon ) === getSortValue ( mod . species . get ( teraFormeChangesFrom ) ) ) continue ;
1493
+ if ( mon . isNonstandard === 'Gigantamax' && ! allowGmax ) continue ;
1494
+ results . push ( mon ) ;
1402
1495
}
1403
1496
1404
1497
if ( usedMod === 'gen7letsgo' ) {
1405
- results = results . filter ( name => {
1406
- const species = mod . species . get ( name ) ;
1498
+ results = results . filter ( species => {
1407
1499
return ( species . num <= 151 || [ 'Meltan' , 'Melmetal' ] . includes ( species . name ) ) &&
1408
1500
( ! species . forme || ( [ 'Alola' , 'Mega' , 'Mega-X' , 'Mega-Y' , 'Starter' ] . includes ( species . forme ) &&
1409
1501
species . name !== 'Pikachu-Alola' ) ) ;
1410
1502
} ) ;
1411
1503
}
1412
1504
1413
1505
if ( usedMod === 'gen8bdsp' ) {
1414
- results = results . filter ( name => {
1415
- const species = mod . species . get ( name ) ;
1506
+ results = results . filter ( species => {
1416
1507
if ( species . id === 'pichuspikyeared' ) return false ;
1417
1508
if ( capSearch ) return species . gen <= 4 ;
1418
1509
return species . gen <= 4 && species . num >= 1 ;
@@ -1428,15 +1519,15 @@ function runDexsearch(target: string, cmd: string, canAll: boolean, message: str
1428
1519
results . sort ( ) ;
1429
1520
if ( sort ) {
1430
1521
const direction = sort . slice ( - 1 ) ;
1431
- Utils . sortBy ( results , name => getSortValue ( name ) * ( direction === '+' ? 1 : - 1 ) ) ;
1522
+ Utils . sortBy ( results , species => getSortValue ( species ) * ( direction === '+' ? 1 : - 1 ) ) ;
1432
1523
}
1433
1524
let notShown = 0 ;
1434
1525
if ( ! showAll && results . length > MAX_RANDOM_RESULTS ) {
1435
1526
notShown = results . length - RESULTS_MAX_LENGTH ;
1436
1527
results = results . slice ( 0 , RESULTS_MAX_LENGTH ) ;
1437
1528
}
1438
1529
resultsStr += results . map (
1439
- result => `<a href="//${ Config . routes . dex } /pokemon/${ toID ( result ) } " target="_blank" class="subtle" style="white-space:nowrap"><psicon pokemon="${ result } " style="vertical-align:-7px;margin:-2px" />${ result } </a>`
1530
+ result => `<a href="//${ Config . routes . dex } /pokemon/${ toID ( result . name ) } " target="_blank" class="subtle" style="white-space:nowrap"><psicon pokemon="${ result . name } " style="vertical-align:-7px;margin:-2px" />${ result . name } </a>`
1440
1531
) . join ( ", " ) ;
1441
1532
if ( notShown ) {
1442
1533
resultsStr += `, and ${ notShown } more. <span style="color:#999999;">Redo the search with ', all' at the end to show all results.</span>` ;
@@ -1446,7 +1537,7 @@ function runDexsearch(target: string, cmd: string, canAll: boolean, message: str
1446
1537
} else {
1447
1538
resultsStr += "No Pokémon found." ;
1448
1539
}
1449
- if ( isTest ) return { results, reply : resultsStr } ;
1540
+ if ( isTest ) return { results : results . map ( species => species . name ) , reply : resultsStr } ;
1450
1541
return { reply : resultsStr } ;
1451
1542
}
1452
1543
0 commit comments