From 29f778f233bb7dabcf48b86fde69f90df7f494c8 Mon Sep 17 00:00:00 2001 From: Ivo Julca Date: Mon, 13 Apr 2026 14:02:48 -0500 Subject: [PATCH] Stricter typing for formats and rules - Legacy effect type ``Ruleset`` removed - New type ``RuleValueType`` for ``rule.hasValue`` - New interface ``RuleTableBuildContext`` for ``onValidateRule`` handlers - New interface: ``RuleEventMethods`` extending ``EventMethods`` - ``data/rulesets.ts``: Different tag-discriminated types for ``effectType='Rule'`` vs ``effectType='ValidatorRule'``: ``RuleData`` vs ``ValidatorRuleData``. - ``data/rulesets.ts``: Renamed ``FormatDataTable`` to ``RulesetTable`` - Distinguishes types for formats in ``config/formats.ts`` vs in ``Dex.data.Rulesets``: ``FormatData`` vs ``LoadedFormatData``. - Union type for formats OR rulesets in ``Dex.data.Rulesets``: ``RulesetData`` - Implements ``DexFormats#find|some``, for searches excluding rules. - Remove obsolete comment regarding former lack of support for format.onResidual --- config/formats.ts | 1 - data/mods/ccapm2025/rulesets.ts | 2 +- data/mods/champions/rulesets.ts | 2 +- data/mods/gen1/rulesets.ts | 2 +- data/mods/gen1jpn/rulesets.ts | 2 +- data/mods/gen1stadium/rulesets.ts | 2 +- data/mods/gen2/rulesets.ts | 2 +- data/mods/gen2stadium2/rulesets.ts | 2 +- data/mods/gen3/rulesets.ts | 2 +- data/mods/gen3frlg/rulesets.ts | 2 +- data/mods/gen4/rulesets.ts | 2 +- data/mods/gen5/rulesets.ts | 2 +- data/mods/gen7/rulesets.ts | 2 +- data/mods/gen7letsgo/rulesets.ts | 2 +- data/mods/gen8/rulesets.ts | 2 +- data/mods/gen8dlc1/rulesets.ts | 2 +- data/mods/gen9ssb/rulesets.ts | 2 +- data/rulesets.ts | 11 +- server/chat-plugins/datasearch.ts | 15 +- sim/dex-conditions.ts | 12 ++ sim/dex-formats.ts | 241 ++++++++++++++++++++++++----- sim/dex.ts | 2 +- sim/global-types.ts | 2 + 23 files changed, 253 insertions(+), 63 deletions(-) diff --git a/config/formats.ts b/config/formats.ts index fb5e448eab..34374bbe34 100644 --- a/config/formats.ts +++ b/config/formats.ts @@ -1507,7 +1507,6 @@ export const Formats: import('../sim/dex-formats').FormatList = [ 'Mewtwo', 'Miraidon', 'Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane', 'Palafin', 'Palkia', 'Palkia-Origin', 'Rayquaza', 'Regieleki', 'Reshiram', 'Shaymin-Sky', 'Solgaleo', 'Terapagos', 'Urshifu-Single-Strike', 'Zacian', 'Zacian-Crowned', 'Zamazenta-Crowned', 'Zekrom', ], - // Implemented the mechanics as a Rule because I'm too lazy to make battles read base format for `onResidual` at the moment }, { name: "[Gen 9] Flipped", diff --git a/data/mods/ccapm2025/rulesets.ts b/data/mods/ccapm2025/rulesets.ts index d9b72b0b03..9ef246ea2e 100644 --- a/data/mods/ccapm2025/rulesets.ts +++ b/data/mods/ccapm2025/rulesets.ts @@ -1,4 +1,4 @@ -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { terastalclause: { effectType: 'Rule', name: 'Terastal Clause', diff --git a/data/mods/champions/rulesets.ts b/data/mods/champions/rulesets.ts index 6431ee0edc..ad845cc0d1 100644 --- a/data/mods/champions/rulesets.ts +++ b/data/mods/champions/rulesets.ts @@ -1,4 +1,4 @@ -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { standardag: { inherit: true, ruleset: [ diff --git a/data/mods/gen1/rulesets.ts b/data/mods/gen1/rulesets.ts index d6bfeaa805..616628fd6a 100644 --- a/data/mods/gen1/rulesets.ts +++ b/data/mods/gen1/rulesets.ts @@ -1,4 +1,4 @@ -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { standardag: { inherit: true, ruleset: [ diff --git a/data/mods/gen1jpn/rulesets.ts b/data/mods/gen1jpn/rulesets.ts index 201cc114e6..c408865324 100644 --- a/data/mods/gen1jpn/rulesets.ts +++ b/data/mods/gen1jpn/rulesets.ts @@ -1,4 +1,4 @@ -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { standard: { effectType: 'ValidatorRule', name: 'Standard', diff --git a/data/mods/gen1stadium/rulesets.ts b/data/mods/gen1stadium/rulesets.ts index 6d81137ec5..b7bce8d6c2 100644 --- a/data/mods/gen1stadium/rulesets.ts +++ b/data/mods/gen1stadium/rulesets.ts @@ -1,4 +1,4 @@ -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { standardag: { inherit: true, ruleset: [ diff --git a/data/mods/gen2/rulesets.ts b/data/mods/gen2/rulesets.ts index ad992422e1..19991b5ebc 100644 --- a/data/mods/gen2/rulesets.ts +++ b/data/mods/gen2/rulesets.ts @@ -1,6 +1,6 @@ import type { Learnset } from "../../../sim/dex-species"; -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { obtainablemoves: { inherit: true, banlist: [ diff --git a/data/mods/gen2stadium2/rulesets.ts b/data/mods/gen2stadium2/rulesets.ts index 7da1a4ed93..e322ee8b85 100644 --- a/data/mods/gen2stadium2/rulesets.ts +++ b/data/mods/gen2stadium2/rulesets.ts @@ -1,4 +1,4 @@ -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { standardag: { inherit: true, ruleset: [ diff --git a/data/mods/gen3/rulesets.ts b/data/mods/gen3/rulesets.ts index 637f7d2197..1e9c41091e 100644 --- a/data/mods/gen3/rulesets.ts +++ b/data/mods/gen3/rulesets.ts @@ -1,4 +1,4 @@ -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { standard: { effectType: 'ValidatorRule', name: 'Standard', diff --git a/data/mods/gen3frlg/rulesets.ts b/data/mods/gen3frlg/rulesets.ts index 0dec9020f9..05b25dded2 100644 --- a/data/mods/gen3frlg/rulesets.ts +++ b/data/mods/gen3frlg/rulesets.ts @@ -1,4 +1,4 @@ -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { standard: { effectType: 'ValidatorRule', name: 'Standard', diff --git a/data/mods/gen4/rulesets.ts b/data/mods/gen4/rulesets.ts index 71359c6c5a..3cb69117fb 100644 --- a/data/mods/gen4/rulesets.ts +++ b/data/mods/gen4/rulesets.ts @@ -1,4 +1,4 @@ -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { standardag: { inherit: true, ruleset: [ diff --git a/data/mods/gen5/rulesets.ts b/data/mods/gen5/rulesets.ts index 00e9e52bbf..7678847fd0 100644 --- a/data/mods/gen5/rulesets.ts +++ b/data/mods/gen5/rulesets.ts @@ -1,4 +1,4 @@ -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { standard: { inherit: true, ruleset: [ diff --git a/data/mods/gen7/rulesets.ts b/data/mods/gen7/rulesets.ts index 3416d4e202..6fccc34acd 100644 --- a/data/mods/gen7/rulesets.ts +++ b/data/mods/gen7/rulesets.ts @@ -1,4 +1,4 @@ -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { standard: { inherit: true, ruleset: [ diff --git a/data/mods/gen7letsgo/rulesets.ts b/data/mods/gen7letsgo/rulesets.ts index d01a6d2f48..a5e5603bef 100644 --- a/data/mods/gen7letsgo/rulesets.ts +++ b/data/mods/gen7letsgo/rulesets.ts @@ -1,4 +1,4 @@ -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { standard: { inherit: true, ruleset: [ diff --git a/data/mods/gen8/rulesets.ts b/data/mods/gen8/rulesets.ts index b430474530..20c01032a0 100644 --- a/data/mods/gen8/rulesets.ts +++ b/data/mods/gen8/rulesets.ts @@ -1,4 +1,4 @@ -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { standard: { inherit: true, ruleset: [ diff --git a/data/mods/gen8dlc1/rulesets.ts b/data/mods/gen8dlc1/rulesets.ts index 41a37960bc..62527bafa4 100644 --- a/data/mods/gen8dlc1/rulesets.ts +++ b/data/mods/gen8dlc1/rulesets.ts @@ -1,4 +1,4 @@ -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { teampreview: { inherit: true, onBattleStart() { diff --git a/data/mods/gen9ssb/rulesets.ts b/data/mods/gen9ssb/rulesets.ts index 890d76a1f6..32eb4e8ed5 100644 --- a/data/mods/gen9ssb/rulesets.ts +++ b/data/mods/gen9ssb/rulesets.ts @@ -1,4 +1,4 @@ -export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable = { +export const Rulesets: import('../../../sim/dex-formats').ModdedRulesetTable = { sleepclausemod: { inherit: true, onSetStatus(status, target, source) { diff --git a/data/rulesets.ts b/data/rulesets.ts index 22916b6a75..4c0db2e3e1 100644 --- a/data/rulesets.ts +++ b/data/rulesets.ts @@ -3,7 +3,7 @@ import type { Learnset } from "../sim/dex-species"; // The list of formats is stored in config/formats.js -export const Rulesets: import('../sim/dex-formats').FormatDataTable = { +export const Rulesets: import('../sim/dex-formats').RulesetTable = { // Rulesets /////////////////////////////////////////////////////////////////// @@ -564,7 +564,7 @@ export const Rulesets: import('../sim/dex-formats').FormatDataTable = { }, }, forceselect: { - effectType: 'ValidatorRule', + effectType: 'Rule', name: 'Force Select', desc: `Forces a Pokemon to be on the team and selected at Team Preview. Usage: Force Select = [Pokemon], e.g. "Force Select = Magikarp"`, hasValue: true, @@ -3253,8 +3253,11 @@ export const Rulesets: import('../sim/dex-formats').FormatDataTable = { if (this.ruleTable.adjustLevel) { throw new Error(`This format's rules force Pokemon to be level ${this.ruleTable.adjustLevel}, so they can't be rebalanced.`); } - const speciesMods = [...this.ruleTable.keys()].map(r => this.dex.data.Rulesets[r]).filter(r => r?.onModifySpecies); - if (!speciesMods.length) throw new Error('This format has no rules that modify base stats.'); + const anySpeciesMods = [...this.ruleTable.keys()].some(ruleName => { + const rule = this.dex.data.Rulesets[ruleName]; + return rule && rule.effectType !== 'ValidatorRule' && rule.onModifySpecies; + }); + if (!anySpeciesMods) throw new Error('This format has no rules that modify base stats.'); }, }, }; diff --git a/server/chat-plugins/datasearch.ts b/server/chat-plugins/datasearch.ts index 140ef725c5..9794f2f267 100644 --- a/server/chat-plugins/datasearch.ts +++ b/server/chat-plugins/datasearch.ts @@ -10,7 +10,7 @@ import * as ConfigLoader from '../config-loader'; import { ProcessManager, Utils } from '../../lib'; -import type { FormatData } from '../../sim/dex-formats'; +import type { RulesetData } from '../../sim/dex-formats'; import { TeamValidator } from '../../sim/team-validator'; import { Chat } from '../chat'; @@ -635,8 +635,12 @@ function getRule(target: string) { 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'; +function prepareDexsearchValidator( + usedMod: string | undefined, + rules: RulesetData[], + nationalSearch: boolean | null +) { + const format = Dex.formats.find(f => f.mod === usedMod)?.name || 'gen9ou'; const ruleTable = Dex.formats.getRuleTable(Dex.formats.get(format)); const additionalRules = []; for (const rule of rules) { @@ -661,7 +665,7 @@ function runDexsearch(target: string, cmd: string, message: string, isTest: bool } } const mod = Dex.mod(usedMod || 'base'); - const rules: FormatData[] = []; + const rules: RulesetData[] = []; for (const rule of usedRules) { if (!dexsearchHelpRules.includes(rule)) return { error: `${rule} is an unsupported rule, see /dexsearchhelp` }; @@ -1316,7 +1320,8 @@ function runDexsearch(target: string, cmd: string, message: string, isTest: bool ) { let newSpecies = species; for (const rule of rules) { - newSpecies = rule?.onModifySpecies?.call({ dex: mod, clampIntRange: Utils.clampIntRange, toID } as Battle, + if (!rule || rule.effectType === 'ValidatorRule') continue; + newSpecies = rule.onModifySpecies?.call({ dex: mod, clampIntRange: Utils.clampIntRange, toID } as Battle, newSpecies) || newSpecies; } dex[newSpecies.id] = newSpecies; diff --git a/sim/dex-conditions.ts b/sim/dex-conditions.ts index 4fd867aaf0..498d017bee 100644 --- a/sim/dex-conditions.ts +++ b/sim/dex-conditions.ts @@ -125,6 +125,9 @@ export interface EventMethods { onWeatherModifyDamage?: CommonHandlers['ModifierSourceMove']; onModifyDamagePhase1?: CommonHandlers['ModifierSourceMove']; onModifyDamagePhase2?: CommonHandlers['ModifierSourceMove']; + onModifySpecies?: ( + this: Battle, species: Species, target?: Pokemon, source?: Pokemon, effect?: Effect + ) => Species | void; onFoeDamagingHit?: (this: Battle, damage: number, target: Pokemon, source: Pokemon, move: ActiveMove) => void; onFoeAfterEachBoost?: (this: Battle, boost: SparseBoostsTable, target: Pokemon, source: Pokemon) => void; onFoeAfterHit?: MoveEventMethods['onAfterHit']; @@ -472,6 +475,7 @@ export interface EventMethods { onModifySpAPriority?: number; onModifySpDPriority?: number; onModifySpePriority?: number; + onModifySpeciesPriority?: number; onModifySTABPriority?: number; onModifyTypePriority?: number; onModifyWeightPriority?: number; @@ -615,6 +619,14 @@ export interface FieldEventMethods extends EventMethods { onFieldResidualPriority?: number; onFieldResidualSubOrder?: number; } +export interface RuleEventMethods extends EventMethods { + onBegin?: (this: Battle) => void; + onBattleStart?: (this: Battle) => void; + onTeamPreview?: (this: Battle) => void; + onChooseTeam?: ( + this: Battle, positions: number[], pokemon: Pokemon[], autoChoose?: boolean + ) => number[] | string | void; +} export interface PokemonConditionData extends Partial, PokemonEventMethods {} export interface SideConditionData extends Partial>, SideEventMethods {} diff --git a/sim/dex-formats.ts b/sim/dex-formats.ts index e093257cb2..4c00959d00 100644 --- a/sim/dex-formats.ts +++ b/sim/dex-formats.ts @@ -1,21 +1,174 @@ import { Utils } from '../lib/utils'; import { assignMissingFields, toID, BasicEffect } from './dex-data'; -import type { EventMethods } from './dex-conditions'; +import type { RuleEventMethods } from './dex-conditions'; import type { SpeciesData } from './dex-species'; import { Tags } from '../data/tags'; const DEFAULT_MOD = 'gen9'; -export interface FormatData extends Partial, EventMethods { - name: string; +export type RuleValueType = boolean | 'integer' | 'positive-integer'; + +/** Used for onValidateRule handlers */ +export interface RuleTableBuildContext { + format: Format; + ruleTable: RuleTable; + dex: ModdedDex; } -export type FormatList = (FormatData | { section: string, column?: number })[]; -export type ModdedFormatData = FormatData | Omit & { inherit: true }; -export interface FormatDataTable { [id: IDEntry]: FormatData } -export interface ModdedFormatDataTable { [id: IDEntry]: ModdedFormatData } +type NamedBasicEffectFragment = Omit, 'name'>, 'effectType'>; -type FormatEffectType = 'Format' | 'Ruleset' | 'Rule' | 'ValidatorRule'; +interface ValidatorRuleFields { + /** List of rule names. */ + ruleset?: string[]; + /** List of banned effects. */ + banlist?: string[]; + /** List of effects that aren't completely banned. */ + restricted?: string[]; + /** List of inherited banned effects to override. */ + unbanlist?: string[]; + + /** Needed in order to print clauses */ + onBegin?: RuleEventMethods['onBegin']; + checkCanLearn?: ( + this: TeamValidator, move: Move, species: Species, setSources: PokemonSources, set: PokemonSet + ) => string | null; + onChangeSet?: ( + this: TeamValidator, set: PokemonSet, format: Format, setHas?: AnyObject, teamHas?: AnyObject + ) => string[] | void; + onValidateSet?: ( + this: TeamValidator, set: PokemonSet, format: Format, setHas: AnyObject, teamHas: AnyObject + ) => string[] | void; + onValidateTeam?: ( + this: TeamValidator, team: PokemonSet[], format: Format, teamHas: AnyObject + ) => string[] | void; + + /** ID of rule that can't be combined with this rule */ + mutuallyExclusiveWith?: string; +} + +interface RuleFields extends ValidatorRuleFields, RuleEventMethods {} + +interface FormatFields extends RuleFields { + mod?: string; + /** + * Name of the team generator algorithm, if this format uses + * random/fixed teams. null if players can bring teams. + */ + team?: string; + debug?: boolean; + noLog?: boolean; + + /** + * Whether or not a format will update ladder points if searched + * for using the "Battle!" button. + * (Challenge and tournament games will never update ladder points.) + * (Defaults to `true`.) + */ + rated?: boolean | string; + /** Game type. */ + gameType?: GameType; + + threads?: string[]; + + /** Overrides for battle scripts */ + battle?: ModdedBattleScriptsData; + pokemon?: ModdedBattlePokemon; + queue?: ModdedBattleQueue; + field?: ModdedField; + actions?: ModdedBattleActions; + side?: ModdedBattleSide; + + /** Flags for the formats list */ + challengeShow?: boolean; + searchShow?: boolean; + tournamentShow?: boolean; + bestOfDefault?: boolean; + teraPreviewDefault?: boolean; + itemClauseDefault?: boolean; + + /** Validator overrides */ + validateSet?: (this: TeamValidator, set: PokemonSet, teamHas: AnyObject) => string[] | null; + validateTeam?: (this: TeamValidator, team: PokemonSet[], options?: { + removeNicknames?: boolean, + skipSets?: { [name: string]: { [key: string]: boolean } }, + }) => string[] | void; + + // OMs + getEvoFamily?: (this: Format, speciesid: string) => ID; + getSharedPower?: (this: Format, pokemon: Pokemon) => Set; + getSharedItems?: (this: Format, pokemon: Pokemon) => Set; +} + +interface TaggedValidatorRuleFields extends ValidatorRuleFields { + effectType: 'ValidatorRule'; + + /** + * Only applies to rules, not formats + */ + hasValue?: RuleValueType; + onValidateRule?: (this: RuleTableBuildContext, value: string) => string | void; +} + +interface TaggedRuleFields extends RuleFields { + effectType: 'Rule'; + + /** + * Only applies to rules, not formats + */ + hasValue?: RuleValueType; + onValidateRule?: (this: RuleTableBuildContext, value: string) => string | void; +} + +interface TaggedFormatFields extends FormatFields { + /** + * A format can be used as a rule, but without an associated value. + */ + onValidateRule?: (this: RuleTableBuildContext) => string | void; +} + +export interface ValidatorRuleData extends NamedBasicEffectFragment, Readonly {} +export interface RuleData extends NamedBasicEffectFragment, Readonly {} +export interface FormatData extends NamedBasicEffectFragment, Readonly {} + +/** Distinguishes types for formats in `config/formats.ts` vs in `Dex.data.Rulesets` */ +export interface LoadedFormatData extends FormatData { + effectType: 'Format'; + section: string; + column: number; + ruleTable: RuleTable | null; +} + +type FormatDataVariantMap = { + Format: FormatData, + Rule: RuleData, + ValidatorRule: ValidatorRuleData, +}; + +export type FormatEffectType = keyof FormatDataVariantMap; +export type RulesetEffectType = Exclude; +type FormatDataVariant = FormatDataVariantMap[K]; +export type GeneralizedFormatData = FormatDataVariant; +type GeneralizedRuleData = FormatDataVariant; + +export type ModdedRuleData = RuleData | (Omit< + Omit, + 'effectType' +> & { inherit: true }); +export type ModdedValidatorRuleData = ValidatorRuleData | (Omit< + Omit, + 'effectType' +> & { inherit: true }); +export type ModdedGeneralizedRuleData = GeneralizedRuleData | (Omit< + Omit, + 'effectType' +> & { inherit: true }); + +export type FormatList = (FormatData | { section: string, column?: number })[]; +export interface RulesetTable { [id: IDEntry]: GeneralizedRuleData } +export interface ModdedRulesetTable { [id: IDEntry]: ModdedGeneralizedRuleData } + +/** Union type for formats OR rules in `Dex.data.Rulesets` */ +export type RulesetData = (LoadedFormatData | GeneralizedRuleData); /** rule, source, limit, bans */ export type ComplexBan = [string, string, number, string[]]; @@ -46,7 +199,7 @@ export class RuleTable extends Map { complexBans: ComplexBan[]; complexTeamBans: ComplexTeamBan[]; checkCanLearn: [TeamValidator['checkCanLearn'], string] | null; - onChooseTeam: [NonNullable, string] | null; + onChooseTeam: [NonNullable, string] | null; timer: [Partial, string] | null; tagRules: string[]; valueRules: Map; @@ -375,7 +528,7 @@ export class RuleTable extends Map { } } -export class Format extends BasicEffect implements Readonly { +export class Format extends BasicEffect implements Readonly, RuleEventMethods { readonly mod: string; /** * Name of the team generator algorithm, if this format uses @@ -384,6 +537,7 @@ export class Format extends BasicEffect implements Readonly { declare readonly team?: string; override readonly effectType: FormatEffectType; readonly debug: boolean; + readonly noLog: boolean; /** * Whether or not a format will update ladder points if searched * for using the "Battle!" button. @@ -412,16 +566,13 @@ export class Format extends BasicEffect implements Readonly { readonly customRules: string[] | null; /** Table of rule names and banned effects. */ ruleTable: RuleTable | null; - /** An optional function that runs at the start of a battle. */ - readonly onBegin?: (this: Battle) => void; - readonly noLog: boolean; /** * Only applies to rules, not formats */ - declare readonly hasValue?: boolean | 'integer' | 'positive-integer'; + declare readonly hasValue?: RuleValueType; declare readonly onValidateRule?: ( - this: { format: Format, ruleTable: RuleTable, dex: ModdedDex }, value: string + this: RuleTableBuildContext, value: string ) => string | void; /** ID of rule that can't be combined with this rule */ declare readonly mutuallyExclusiveWith?: string; @@ -432,49 +583,54 @@ export class Format extends BasicEffect implements Readonly { declare readonly field?: ModdedField; declare readonly actions?: ModdedBattleActions; declare readonly side?: ModdedBattleSide; + declare readonly challengeShow?: boolean; declare readonly searchShow?: boolean; + declare readonly tournamentShow?: boolean; declare readonly bestOfDefault?: boolean; declare readonly teraPreviewDefault?: boolean; declare readonly itemClauseDefault?: boolean; declare readonly threads?: string[]; - declare readonly tournamentShow?: boolean; + declare readonly checkCanLearn?: ( this: TeamValidator, move: Move, species: Species, setSources: PokemonSources, set: PokemonSet ) => string | null; - declare readonly getEvoFamily?: (this: Format, speciesid: string) => ID; - declare readonly getSharedPower?: (this: Format, pokemon: Pokemon) => Set; - declare readonly getSharedItems?: (this: Format, pokemon: Pokemon) => Set; declare readonly onChangeSet?: ( this: TeamValidator, set: PokemonSet, format: Format, setHas?: AnyObject, teamHas?: AnyObject ) => string[] | void; - declare readonly onModifySpeciesPriority?: number; - declare readonly onModifySpecies?: ( - this: Battle, species: Species, target?: Pokemon, source?: Pokemon, effect?: Effect - ) => Species | void; - declare readonly onBattleStart?: (this: Battle) => void; - declare readonly onTeamPreview?: (this: Battle) => void; - declare readonly onChooseTeam?: ( - this: Battle, positions: number[], pokemon: Pokemon[], autoChoose?: boolean - ) => number[] | string | void; declare readonly onValidateSet?: ( this: TeamValidator, set: PokemonSet, format: Format, setHas: AnyObject, teamHas: AnyObject ) => string[] | void; declare readonly onValidateTeam?: ( this: TeamValidator, team: PokemonSet[], format: Format, teamHas: AnyObject ) => string[] | void; + declare readonly validateSet?: (this: TeamValidator, set: PokemonSet, teamHas: AnyObject) => string[] | null; declare readonly validateTeam?: (this: TeamValidator, team: PokemonSet[], options?: { removeNicknames?: boolean, skipSets?: { [name: string]: { [key: string]: boolean } }, }) => string[] | void; + + declare readonly onBegin?: RuleEventMethods['onBegin']; + declare readonly onBattleStart?: RuleEventMethods['onBattleStart']; + declare readonly onTeamPreview?: RuleEventMethods['onTeamPreview']; + declare readonly onChooseTeam?: RuleEventMethods['onChooseTeam']; + + declare readonly onModifySpeciesPriority?: RuleEventMethods['onModifySpeciesPriority']; + declare readonly onModifySpecies?: RuleEventMethods['onModifySpecies']; + declare readonly section?: string; declare readonly column?: number; + // OMs + getEvoFamily?: (this: Format, speciesid: string) => ID; + getSharedPower?: (this: Format, pokemon: Pokemon) => Set; + getSharedItems?: (this: Format, pokemon: Pokemon) => Set; + constructor(data: AnyObject) { super(data); - this.mod = Utils.getString(data.mod) || 'gen9'; + this.mod = Utils.getString(data.mod) || DEFAULT_MOD; this.effectType = Utils.getString(data.effectType) as FormatEffectType || 'Condition'; this.debug = !!data.debug; this.rated = (typeof data.rated === 'string' ? data.rated : data.rated !== false); @@ -514,11 +670,11 @@ function mergeFormatLists(main: FormatList, custom: FormatList | undefined): For // populates the original sections and formats easily // there should be no repeat sections at this point. for (const element of main) { - if (element.section) { + if ('section' in element) { current = { section: element.section, column: element.column, formats: [] }; build.push(current); - } else if ((element as FormatData).name) { - current.formats.push((element as FormatData)); + } else if (element.name) { + current.formats.push(element); } } @@ -526,7 +682,7 @@ function mergeFormatLists(main: FormatList, custom: FormatList | undefined): For if (custom !== undefined) { for (const element of custom) { // finds the section and makes it if it doesn't exist. - if (element.section) { + if ('section' in element) { current = build.find(e => e.section === element.section); // if it's new it makes a new entry. @@ -534,8 +690,8 @@ function mergeFormatLists(main: FormatList, custom: FormatList | undefined): For current = { section: element.section, column: element.column, formats: [] }; build.push(current); } - } else if ((element as FormatData).name) { // otherwise, adds the element to its section. - current.formats.push(element as FormatData); + } else if (element.name) { // otherwise, adds the element to its section. + current.formats.push(element); } } } @@ -605,7 +761,7 @@ export class DexFormats { if (format.bestOfDefault === undefined) format.bestOfDefault = false; if (format.teraPreviewDefault === undefined) format.teraPreviewDefault = false; if (format.itemClauseDefault === undefined) format.itemClauseDefault = false; - if (format.mod === undefined) format.mod = 'gen9'; + if (format.mod === undefined) format.mod = DEFAULT_MOD; if (!this.dex.dexes[format.mod]) throw new Error(`Format "${format.name}" requires nonexistent mod: '${format.mod}'`); this.checkDeprecated(format); @@ -729,6 +885,19 @@ export class DexFormats { return this.formatsListCache!; } + find(filterFn: (format: Format) => boolean): Format | null { + this.load(); + for (const format of this.formatsListCache!) { + if (filterFn(format)) return format; + } + return null; + } + + filter(filterFn: (format: Format) => boolean): Format[] { + this.load(); + return this.formatsListCache!.filter(filterFn); + } + isPokemonRule(ruleSpec: string) { return ( ruleSpec.slice(1).startsWith('pokemontag:') || ruleSpec.slice(1).startsWith('pokemon:') || diff --git a/sim/dex.ts b/sim/dex.ts index edaf6e6917..eddba86eed 100644 --- a/sim/dex.ts +++ b/sim/dex.ts @@ -73,7 +73,7 @@ export interface AliasesTable { [id: IDEntry]: string } interface DexTableData { Abilities: DexTable; - Rulesets: DexTable; + Rulesets: DexTable; Items: DexTable; Learnsets: DexTable; Moves: DexTable; diff --git a/sim/global-types.ts b/sim/global-types.ts index d339924211..4b4ab0fcb7 100644 --- a/sim/global-types.ts +++ b/sim/global-types.ts @@ -4,6 +4,8 @@ type Mutable = { -readonly [P in keyof T]: T[P]; }; +type WithRequired = Partial & Required>; + type Battle = import('./battle').Battle; type BattleQueue = import('./battle-queue').BattleQueue; type BattleActions = import('./battle-actions').BattleActions;