diff --git a/play.pokemonshowdown.com/src/battle-dex-data.ts b/play.pokemonshowdown.com/src/battle-dex-data.ts
index 69c8f5e16..39d8eeebf 100644
--- a/play.pokemonshowdown.com/src/battle-dex-data.ts
+++ b/play.pokemonshowdown.com/src/battle-dex-data.ts
@@ -21,6 +21,89 @@ import { Dex, toID } from "./battle-dex";
*/
export type ID = string & { __isID: true };
+export interface FormatData {
+ id: ID;
+ name: string;
+ team?: 'preset' | null;
+ section: string;
+ column: number;
+ searchShow?: boolean;
+ challengeShow?: boolean;
+ tournamentShow?: boolean;
+ bestOfDefault?: boolean;
+ teraPreviewDefault?: boolean;
+ itemClauseDefault?: boolean;
+ rated: boolean;
+ teambuilderLevel?: number | null;
+ partner?: boolean;
+ teambuilderFormat?: ID;
+ battleFormat?: string;
+ isTeambuilderFormat: boolean;
+ effectType: 'Format';
+}
+
+export class Format implements FormatData {
+ id: ID;
+ name: string;
+ readonly effectType = 'Format';
+ gen: number;
+ mod: ID;
+ team: 'preset' | null = null;
+ section = '';
+ column = 0;
+ searchShow?: boolean = undefined;
+ challengeShow?: boolean = undefined;
+ tournamentShow?: boolean = undefined;
+ bestOfDefault?: boolean = undefined;
+ teraPreviewDefault?: boolean = undefined;
+ itemClauseDefault?: boolean = undefined;
+ rated = false;
+ teambuilderLevel?: number | null = undefined;
+ partner?: boolean = undefined;
+ teambuilderFormat?: ID = undefined;
+ battleFormat?: string = undefined;
+ isTeambuilderFormat = false;
+ isLetsGo: boolean;
+ isNatDex: boolean;
+ isBDSP: boolean;
+ isChampions: boolean;
+ allowMultipleMegas: boolean;
+ formeLegality: 'normal' | 'hackmons' | 'custom' = 'normal';
+ abilityLegality: 'normal' | 'hackmons' = 'normal';
+ defaultLevel = 100;
+
+ constructor(name: string, data?: FormatData) {
+ this.id = toID(name);
+ this.name = name;
+ const id = this.id;
+ this.gen = !id ? Dex.gen : id.startsWith('gen') ? parseInt(id.charAt(3)) || Dex.gen : 6;
+ this.isLetsGo = id.includes('letsgo');
+ this.isNatDex = id.includes('nationaldex') || id.includes('natdex');
+ this.isBDSP = id.includes('bdsp');
+ this.isChampions = id.includes('champions');
+ this.allowMultipleMegas = id.includes('mega');
+ this.mod = `gen${this.gen}` as ID;
+ if (this.gen === 7 && this.isLetsGo) this.mod = 'gen7letsgo' as ID;
+ if (this.gen === 8 && this.isBDSP) this.mod = 'gen8bdsp' as ID;
+ if (this.gen === 9 && this.isChampions) this.mod = 'champions' as ID;
+ if (id.includes('almostanyability') || id.includes('aaa')) this.abilityLegality = 'hackmons';
+ if (id.includes('hackmons') || id.includes('bh')) {
+ this.formeLegality = 'hackmons';
+ this.abilityLegality = 'hackmons';
+ } else if (id.includes('metronome') || id.includes('customgame')) {
+ this.formeLegality = 'custom';
+ this.abilityLegality = 'hackmons';
+ }
+ if (
+ id.includes('vgc') || id.includes('bss') || id.includes('ultrasinnohclassic') ||
+ id.includes('battlespot') || id.includes('battlestadium') || id.includes('battlefestival') ||
+ this.isLetsGo || this.isChampions
+ ) this.defaultLevel = 50;
+ if (id.includes('lc')) this.defaultLevel = 5;
+ if (data) Object.assign(this, data);
+ }
+}
+
export interface Nature {
plus?: StatNameExceptHP;
minus?: StatNameExceptHP;
@@ -1700,4 +1783,5 @@ if (typeof require === 'function') {
global.Ability = Ability;
global.Item = Item;
global.Move = Move;
+ global.Format = Format;
}
diff --git a/play.pokemonshowdown.com/src/battle-dex.ts b/play.pokemonshowdown.com/src/battle-dex.ts
index abe1ff147..9f861546a 100644
--- a/play.pokemonshowdown.com/src/battle-dex.ts
+++ b/play.pokemonshowdown.com/src/battle-dex.ts
@@ -21,7 +21,7 @@
import { Pokemon, type ServerPokemon } from "./battle";
import {
BattleAvatarNumbers, BattleBaseSpeciesChart, BattlePokemonIconIndexes, BattlePokemonIconIndexesLeft,
- Ability, Item, Move, Species, PureEffect, type ID, type NatureEffect, type Type,
+ Ability, Item, Move, Species, PureEffect, Format, type FormatData, type ID, type NatureEffect, type Type,
} from "./battle-dex-data";
import type * as DexData from "./battle-dex-data";
import type { Teams } from "./battle-teams";
@@ -34,6 +34,7 @@ export declare namespace Dex {
export type Item = DexData.Item;
export type Move = DexData.Move;
export type Species = DexData.Species;
+ export type Format = DexData.Format;
export type Type = DexData.Type;
export type Nature = DexData.Nature;
export type PureEffect = DexData.PureEffect;
@@ -513,26 +514,31 @@ export const Dex = new class implements ModdedDex {
if (!gen) return this;
return this.mod(`gen${gen}` as ID);
}
+ formats = {
+ cache: Object.create(null) as { [id: string]: Format },
+ get(name: string): Format {
+ const id = toID(name);
+ this.cache[id] ||= new Format(name, window.BattleFormats?.[id]);
+ return this.cache[id];
+ },
+ load(data: { [id: string]: FormatData }): { [id: string]: Format } {
+ // Update in-place so retained references get updated too
+ for (const id in this.cache) {
+ Object.assign(this.cache[id], new Format(this.cache[id].name, data[id]));
+ }
+ const formats: { [id: string]: Format } = {};
+ for (const id in data) {
+ const format = this.cache[id] ||= new Format(data[id].name, data[id]);
+ formats[id] = format;
+ }
+ return formats;
+ },
+ };
formatGen(format: string) {
- const formatid = toID(format);
- if (!formatid) return Dex.gen;
- if (!formatid.startsWith('gen')) return 6;
- return parseInt(formatid.charAt(3)) || Dex.gen;
+ return this.formats.get(format).gen;
}
forFormat(format: string) {
- let dex = Dex.forGen(Dex.formatGen(format));
-
- const formatid = toID(format).slice(4);
- if (dex.gen === 7 && formatid.includes('letsgo')) {
- dex = Dex.mod('gen7letsgo' as ID);
- }
- if (dex.gen === 8 && formatid.includes('bdsp')) {
- dex = Dex.mod('gen8bdsp' as ID);
- }
- if (dex.gen === 9 && formatid.includes('champions')) {
- dex = Dex.mod('champions' as ID);
- }
- return dex;
+ return this.mod(this.formats.get(format).mod);
}
resolveAvatar(avatar: string): string {
diff --git a/play.pokemonshowdown.com/src/battle-team-editor.tsx b/play.pokemonshowdown.com/src/battle-team-editor.tsx
index b3d3300aa..da9d19480 100644
--- a/play.pokemonshowdown.com/src/battle-team-editor.tsx
+++ b/play.pokemonshowdown.com/src/battle-team-editor.tsx
@@ -61,18 +61,11 @@ export class TeamEditorState extends PSModel {
index: number,
} | null = null;
search = new DexSearch();
- format: ID = `gen${this.gen}` as ID;
+ format = Dex.formats.get(`gen${this.gen}`);
originalSpecies: string | null = null;
narrow = false;
narrowStats = false;
innerFocus: InnerFocusState | null = null;
- isLetsGo = false;
- isNatDex = false;
- isBDSP = false;
- isChampions = false;
- formeLegality: 'normal' | 'hackmons' | 'custom' = 'normal';
- abilityLegality: 'normal' | 'hackmons' = 'normal';
- defaultLevel = 100;
readonly = false;
fetching = false;
handleParentKeyDown?: (ev: KeyboardEvent) => boolean | void;
@@ -92,45 +85,12 @@ export class TeamEditorState extends PSModel {
this.readonly = readonly;
}
setFormat(format: string) {
- const team = this.team;
- const formatid = toID(format);
- this.format = formatid;
- team.format = formatid;
- this.dex = Dex.forFormat(formatid);
+ this.format = Dex.formats.get(format);
+ this.team.format = this.format.id;
+ this.dex = Dex.mod(this.format.mod);
this.gen = this.dex.gen;
-
- format = toID(format).slice(4);
- this.isLetsGo = formatid.includes('letsgo');
- this.isNatDex = formatid.includes('nationaldex') || formatid.includes('natdex');
- this.isBDSP = formatid.includes('bdsp');
- this.isChampions = formatid.includes('champions');
- if (formatid.includes('almostanyability') || formatid.includes('aaa')) {
- this.abilityLegality = 'hackmons';
- } else {
- this.abilityLegality = 'normal';
- }
- if (formatid.includes('hackmons') || formatid.includes('bh')) {
- this.formeLegality = 'hackmons';
- this.abilityLegality = 'hackmons';
- } else if (formatid.includes('metronome') || formatid.includes('customgame')) {
- this.formeLegality = 'custom';
- this.abilityLegality = 'hackmons';
- } else {
- this.formeLegality = 'normal';
- }
-
- this.defaultLevel = 100;
- if (
- formatid.includes('vgc') || formatid.includes('bss') || formatid.includes('ultrasinnohclassic') ||
- formatid.includes('battlespot') || formatid.includes('battlestadium') || formatid.includes('battlefestival') ||
- formatid.includes('letsgo') || formatid.includes('champions')
- ) {
- this.defaultLevel = 50;
- }
- if (formatid.includes('lc')) {
- this.defaultLevel = 5;
- }
}
+
stringifyFocus(focus: FocusState | null): string {
if (!focus) return '';
return `set-${focus.setIndex}-${focus.type}${focus.typeIndex >= 0 ? `-${focus.typeIndex}` : ''}`;
@@ -194,7 +154,7 @@ export class TeamEditorState extends PSModel {
}
setSearchType(type: SearchType, i: number, value?: string, typeIndex = -1) {
const set = this.sets[i];
- this.search.setType(type, this.format, set);
+ this.search.setType(type, this.format.id, set);
this.originalSpecies = null;
this.search.prependResults = null;
if (type === 'move') {
@@ -467,10 +427,10 @@ export class TeamEditorState extends PSModel {
return this.sets.length < 6 || this.team.isBox;
}
showItem(set: Dex.PokemonSet) {
- return !!(this.gen > 1 && !this.isLetsGo || set.item);
+ return !!(this.gen > 1 && !this.format.isLetsGo || set.item);
}
showAbility(set: Dex.PokemonSet) {
- return !!(this.gen > 2 && !this.isLetsGo || set.ability);
+ return !!(this.gen > 2 && !this.format.isLetsGo || set.ability);
}
getHPType(set: Dex.PokemonSet): Dex.TypeName {
if (set.hpType) return set.hpType as Dex.TypeName;
@@ -537,7 +497,7 @@ export class TeamEditorState extends PSModel {
defaultIVs(set: Dex.PokemonSet, noGuess = !!set.ivs): Record {
const useIVs = this.gen > 2;
const defaultIVs = { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31 };
- if (this.isChampions) return defaultIVs;
+ if (this.format.isChampions) return defaultIVs;
if (!useIVs) {
for (const stat of Dex.statNames) defaultIVs[stat] = 15;
}
@@ -595,11 +555,11 @@ export class TeamEditorState extends PSModel {
if (set.species.startsWith('Terapagos')) minSpe = false;
const preferMaxAtkFormats = ['1v1', 'categoryswap', 'noholdsbarred', 'partnersincrime', 'typesplit'];
- if (preferMaxAtkFormats.some(f => this.format.includes(f))) {
+ if (preferMaxAtkFormats.some(f => this.format.id.includes(f))) {
minAtk = false;
return { minAtk, minSpe };
}
- if (this.format === 'gen7hiddentype') return { minAtk, minSpe };
+ if (this.format.id === 'gen7hiddentype') return { minAtk, minSpe };
// only available through an event with 31 Atk IVs
if (set.ability === 'Battle Bond' || ['Koraidon', 'Miraidon', 'Gimmighoul-Roaming'].includes(set.species)) {
@@ -633,10 +593,10 @@ export class TeamEditorState extends PSModel {
return set.name || this.dex.species.get(set.species).baseSpecies || '';
}
canHyperTrain(set: Dex.PokemonSet) {
- let format: string = this.format;
+ let format: string = this.format.id;
if (this.gen < 7 || format === 'gen7hiddentype') return false;
- if ((set.level || this.defaultLevel) === 100) return true;
- if ((set.level || this.defaultLevel) >= 50 && this.defaultLevel === 50) return true;
+ if ((set.level || this.format.defaultLevel) === 100) return true;
+ if ((set.level || this.format.defaultLevel) >= 50 && this.format.defaultLevel === 50) return true;
return false;
}
getHPIVs(hpType: Dex.TypeName | null) {
@@ -683,20 +643,20 @@ export class TeamEditorState extends PSModel {
const species = this.dex.species.get(set.species);
if (!species.exists) return 0;
- const level = set.level || this.defaultLevel;
+ const level = set.level || this.format.defaultLevel;
const baseStat = species.baseStats[stat];
const iv = ivOverride;
let ev = evOverride ?? set.evs?.[stat] ?? (this.gen > 2 ? 0 : 252);
- if (this.isChampions) ev *= 8;
+ if (this.format.isChampions) ev *= 8;
if (stat === 'hp') {
if (baseStat === 1) return 1;
- if (this.isLetsGo) return Math.trunc(Math.trunc(2 * baseStat + iv + 100) * level / 100 + 10) + ev;
+ if (this.format.isLetsGo) return Math.trunc(Math.trunc(2 * baseStat + iv + 100) * level / 100 + 10) + ev;
return Math.trunc(Math.trunc(2 * baseStat + iv + Math.trunc(ev / 4) + 100) * level / 100 + 10);
}
let val = Math.trunc(Math.trunc(2 * baseStat + iv + Math.trunc(ev / 4)) * level / 100 + 5);
- if (this.isLetsGo) {
+ if (this.format.isLetsGo) {
val = Math.trunc(Math.trunc(2 * baseStat + iv) * level / 100 + 5);
}
if (natureOverride) {
@@ -706,7 +666,7 @@ export class TeamEditorState extends PSModel {
} else if (BattleNatures[set.nature!]?.minus === stat) {
val *= 0.9;
}
- if (this.isLetsGo) {
+ if (this.format.isLetsGo) {
const friendshipValue = Math.trunc((70 / 255 / 10 + 1) * 100);
val = Math.trunc(val) * friendshipValue / 100 + ev;
}
@@ -779,13 +739,13 @@ export class TeamEditorState extends PSModel {
return counters;
}
getDefaultAbility(set: Dex.PokemonSet) {
- if (this.gen < 3 || this.isLetsGo || this.formeLegality === 'custom') return set.ability;
+ if (this.gen < 3 || this.format.isLetsGo || this.format.formeLegality === 'custom') return set.ability;
const species = this.dex.species.get(set.species);
- if (this.formeLegality === 'hackmons') {
+ if (this.format.formeLegality === 'hackmons') {
// TODO: support gen 9 hackmons forme legality more completely than this
if (this.gen < 9 || species.baseSpecies !== 'Xerneas') return set.ability;
// falls through to final return statement
- } else if (this.abilityLegality === 'hackmons') {
+ } else if (this.format.abilityLegality === 'hackmons') {
if (!species.battleOnly) return set.ability;
if (species.requiredItems.length || species.baseSpecies === 'Meloetta') return set.ability;
// battle only species only ever have one ability
@@ -801,13 +761,13 @@ export class TeamEditorState extends PSModel {
getDefaultItem(speciesName: string) {
const species = this.dex.species.get(speciesName);
let items = species.requiredItems;
- if (this.gen !== 7 && !this.isNatDex) {
+ if (this.gen !== 7 && !this.format.isNatDex) {
// Require plates on Arceus when Z crystals don't exist
items = items.filter(i => !i.endsWith('ium Z'));
}
if (items.length === 1) {
- if (this.formeLegality === 'normal' ||
- this.formeLegality === 'hackmons' && this.gen === 9 && species.battleOnly &&
+ if (this.format.formeLegality === 'normal' ||
+ this.format.formeLegality === 'hackmons' && this.gen === 9 && species.battleOnly &&
!species.isMega && !species.isPrimal && species.name !== 'Necrozma-Ultra') {
return items[0];
}
@@ -847,9 +807,9 @@ export class TeamEditorState extends PSModel {
}
/** returns null if sample sets aren't done loading */
getSampleSets(set: Dex.PokemonSet): string[] | null {
- const d = TeamEditorState.sampleSets[this.format];
+ const d = TeamEditorState.sampleSets[this.format.id];
if (d === undefined) {
- this.fetchSampleSets(this.format);
+ this.fetchSampleSets(this.format.id);
return null;
}
if (!d?.dex) return [];
@@ -864,11 +824,11 @@ export class TeamEditorState extends PSModel {
}
/** returns null if no boxes exist, empty array if no sets for this species */
getUserSets(set: Dex.PokemonSet): { [setName: string]: Dex.PokemonSet } | null {
- if (!this.userSetsCache[this.format]) {
+ if (!this.userSetsCache[this.format.id]) {
const userSets: { [species: string]: { [setName: string]: Dex.PokemonSet } } = {};
for (const team of window.PS?.teams.list || []) {
- if (team.format !== this.format || !team.isBox) continue;
+ if (team.format !== this.format.id || !team.isBox) continue;
const setList = Teams.unpack(team.packedTeam);
const duplicateNameIndices: Record = {};
@@ -885,10 +845,10 @@ export class TeamEditorState extends PSModel {
}
}
- this.userSetsCache[this.format] = userSets;
+ this.userSetsCache[this.format.id] = userSets;
}
- const cachedSets = this.userSetsCache[this.format];
+ const cachedSets = this.userSetsCache[this.format.id];
if (Object.keys(cachedSets).length === 0) return null;
return cachedSets[set.species] || {};
}
@@ -897,7 +857,7 @@ export class TeamEditorState extends PSModel {
const set = this.sets[setIndex];
if (!set?.species) return false;
- const data = TeamEditorState.sampleSets?.[this.format];
+ const data = TeamEditorState.sampleSets?.[this.format.id];
const sid = toID(set.species);
const setTemplate = data?.dex?.[set.species]?.[setName] ?? data?.dex?.[sid]?.[setName] ??
data?.stats?.[set.species]?.[setName] ?? data?.stats?.[sid]?.[setName];
@@ -1114,7 +1074,7 @@ export class TeamEditor extends preact.Component<{
const narrow = this.props.narrow ?? window.innerWidth < 500;
editor.narrow = !useZoomedOutForms && narrow;
editor.narrowStats = useZoomedOutForms || narrow;
- if (this.props.team.format !== editor.format) {
+ if (this.props.team.format !== editor.format.id) {
editor.setFormat(this.props.team.format);
}
const useSpaciousCSS = spacious && !mobileOptions || useZoomedOutForms;
@@ -1806,12 +1766,12 @@ class TeamTextbox extends preact.Component<{
return
;
}
renderStatOptimizer() {
- const optimized = BattleStatOptimizer(this.props.set, this.props.editor.format);
+ const optimized = BattleStatOptimizer(this.props.set, this.props.editor.format.id);
if (!optimized) return null;
return
@@ -3558,8 +3518,8 @@ class StatForm extends preact.Component<{
renderStatbar(stat: number, statID: StatName) {
const { editor } = this.props;
const maxStat = statID === 'hp' ?
- Math.floor(176 * editor.defaultLevel / 25) + 10 :
- Math.floor(247 * editor.defaultLevel / 50) + 5;
+ Math.floor(176 * editor.format.defaultLevel / 25) + 10 :
+ Math.floor(247 * editor.format.defaultLevel / 50) + 5;
const width = Math.min(stat * 180 / maxStat, 180);
const hue = Math.min(Math.floor(stat * 180 / maxStat), 360);
return previousValue) {
const previousStat = editor.getStat(statID, set, iv, previousValue);
- const maxValue = editor.isChampions ? 32 : editor.isLetsGo ? 200 : 252;
+ const maxValue = editor.format.isChampions ? 32 : editor.format.isLetsGo ? 200 : 252;
while (value < maxValue && editor.getStat(statID, set, iv, value) === previousStat) {
value = Math.min(value + step, maxValue);
}
@@ -3751,8 +3711,8 @@ class StatForm extends preact.Component<{
};
maxEVs() {
const editor = this.props.editor;
- const useCappedEVs = !editor.isLetsGo && editor.gen >= 3 && !editor.isChampions;
- return editor.isChampions ? 66 : useCappedEVs ? 510 : Infinity;
+ const useCappedEVs = !editor.format.isLetsGo && editor.gen >= 3 && !editor.format.isChampions;
+ return editor.format.isChampions ? 66 : useCappedEVs ? 510 : Infinity;
}
override render() {
const { editor, set } = this.props;
@@ -3761,9 +3721,9 @@ class StatForm extends preact.Component<{
const baseStats = species.baseStats;
- const useEVs = !editor.isLetsGo && !editor.isChampions;
- // const useAVs = editor.isLetsGo && team.format.endsWith('norestrictions');
- const maxEV = editor.isChampions ? 32 : useEVs ? 252 : 200;
+ const useEVs = !editor.format.isLetsGo && !editor.format.isChampions;
+ // const useAVs = editor.format.isLetsGo && team.format.endsWith('norestrictions');
+ const maxEV = editor.format.isChampions ? 32 : useEVs ? 252 : 200;
const stepEV = useEVs ? 4 : 1;
const defaultEV = useEVs && editor.gen <= 2 && !set.evs ? maxEV : 0;
const useIVs = editor.gen > 2;
@@ -3782,7 +3742,7 @@ class StatForm extends preact.Component<{
if (maxEVs < 6 * 252) {
let totalEv = 0;
for (const ev of Object.values(set.evs || {})) totalEv += ev;
- if (totalEv <= maxEVs && !editor.isChampions) {
+ if (totalEv <= maxEVs && !editor.format.isChampions) {
remaining = (totalEv > (maxEVs - 2) ? 0 : (maxEVs - 2) - totalEv);
} else {
remaining = maxEVs - totalEv;
@@ -3800,9 +3760,9 @@ class StatForm extends preact.Component<{
{/* Stat name */} |
{TL`Base`} |
{/* Stat bar */} |
- {editor.isLetsGo ? TL`AVs` : editor.isChampions ? TL`Points` : TL`EVs`} |
+ {editor.format.isLetsGo ? TL`AVs` : editor.format.isChampions ? TL`Points` : TL`EVs`} |
{/* EV slider */} |
- {!editor.isChampions && {useIVs ? TL`IVs` : TL`DVs`} | }
+ {!editor.format.isChampions && {useIVs ? TL`IVs` : TL`DVs`} | }
{/* Final stat */} |
{stats.map(([statID, statName, stat]) =>
@@ -3820,7 +3780,7 @@ class StatForm extends preact.Component<{
onPointerDown={this.startEVSliderDrag}
onInput={this.changeEV} onChange={this.changeEV}
/>
- {!editor.isChampions && |
(You probably want to change the team's levels by changing the format, not here)
{editor.gen > 1 && (<>
{TL.label(TL`Shiny`)}
@@ -4017,14 +3977,14 @@ class DetailsForm extends preact.Component<{
/> Random
)}
- {editor.isLetsGo ? (
+ {editor.format.isLetsGo ? (
- ) : (editor.gen < 8 || editor.isNatDex) && (
+ ) : (editor.gen < 8 || editor.format.isNatDex) && (
|