Refactor format metadata
Some checks failed
Node.js CI / build (22.x) (push) Has been cancelled

We were hardcoding format names in a lot of different places, and this
refactor puts them all in the same place (`Dex.formats`) now.

Going forward, the plan is to only use `Dex.formats` to decide what
rules a format does or doesn't have.

This also adds support for Mix & Mega Doubles to newclient.
This commit is contained in:
Guangcong Luo
2026-09-26 23:29:25 -07:00
parent 253053a8e2
commit f514a69b07
9 changed files with 186 additions and 153 deletions

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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<Dex.StatName, number> {
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<string, number> = {};
@@ -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 <button class="textbox setdetails" name="details" value={i} onClick={this.clickDetails}>
<span class="detailcell">
<label>{TL`Level`}</label>{set.level || editor.defaultLevel}
<label>{TL`Level`}</label>{set.level || editor.format.defaultLevel}
</span>
<span class="detailcell">
<label>{TL`Shiny`}</label>{set.shiny ? 'Yes' : '\u2014'}
</span>
{editor.gen === 9 && !editor.isChampions ? (
{editor.gen === 9 && !editor.format.isChampions ? (
<span class="detailcell">
<label>{TL`Tera`}</label><PSIcon type={set.teraType || species.requiredTeraType || species.types[0]} />
</span>
@@ -2997,7 +2957,7 @@ class TeamEditorForm extends preact.Component<{
>
<span class="detailcell">
<label>{TL`Level`}</label> {}
{set.level || editor.defaultLevel}
{set.level || editor.format.defaultLevel}
</span>
{!!(set.shiny || editor.gen >= 2) && <span class="detailcell">
<label>{TL`Shiny`}</label> {}
@@ -3005,7 +2965,7 @@ class TeamEditorForm extends preact.Component<{
src={`${Dex.resourcePrefix}sprites/misc/shiny.png`} width={18} height={18} alt="Yes" style="margin-top: -2px"
/> : '\u2014'}
</span>}
{editor.gen === 9 && !editor.isChampions && <span class="detailcell">
{editor.gen === 9 && !editor.format.isChampions && <span class="detailcell">
<label>{TL`Tera`}</label> {}
<PSIcon type={set.teraType || species.requiredTeraType || species.types[0]} new={!editor.narrow} tera />
</span>}
@@ -3273,8 +3233,8 @@ class StatForm extends preact.Component<{
const stat = editor.getStat(statID, set, ivs[statID]);
let ev: number | string = set.evs ? (set.evs[statID] || 0) : defaultEV;
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 * 75 / maxStat, 75);
const hue = Math.min(Math.floor(stat * 180 / maxStat), 360);
const statName = editor.gen === 1 && statID === 'spa' ? TL.statShort.spc : TL.statShort[statID];
@@ -3302,7 +3262,7 @@ class StatForm extends preact.Component<{
const hpIVdata = hpType && !editor.canHyperTrain(set) && editor.getHPIVs(hpType) || null;
const autoSpread = set.ivs && editor.defaultIVs(set, false);
const autoSpreadValue = autoSpread && Object.values(autoSpread).join('/');
if (editor.isChampions) return null;
if (editor.format.isChampions) return null;
if (!hpIVdata) {
return <select name="ivspread" class="select" onChange={this.changeIVSpread}>
<option value="" selected>{TL`IV spreads`}</option>
@@ -3356,7 +3316,7 @@ class StatForm extends preact.Component<{
smogdexLink(s: string) {
const { editor } = this.props;
const species = editor.dex.species.get(s);
let format: string = editor.format;
let format: string = editor.format.id;
let smogdexid: string = toID(species.baseSpecies);
if (species.id === 'meowstic') {
@@ -3484,7 +3444,7 @@ class StatForm extends preact.Component<{
</p>;
}
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 <p>
@@ -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 <span
@@ -3576,7 +3536,7 @@ class StatForm extends preact.Component<{
if (isNaN(value)) {
if (set.evs) delete set.evs[statID];
} else {
if (this.maxEVs() < 6 * 252 || this.props.editor.isLetsGo) {
if (this.maxEVs() < 6 * 252 || this.props.editor.format.isLetsGo) {
set.evs ||= {};
} else {
set.evs ||= { hp: 252, atk: 252, def: 252, spa: 252, spd: 252, spe: 252 };
@@ -3585,13 +3545,13 @@ class StatForm extends preact.Component<{
}
if (target.type === 'range') {
const step = editor.isLetsGo || editor.isChampions ? 1 : 4;
const step = editor.format.isLetsGo || editor.format.isChampions ? 1 : 4;
const iv = editor.getIVs(set)[statID];
// while dragging, always round down, but other methods (e.g. pressing Right on a keyboard)
// should jump a whole step
if (this.draggingEVSlider !== target && value > 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<{
<th>{/* Stat name */}</th>
<th>{TL`Base`}</th>
<th class="setstatbar">{/* Stat bar */}</th>
<th>{editor.isLetsGo ? TL`AVs` : editor.isChampions ? TL`Points` : TL`EVs`}</th>
<th>{editor.format.isLetsGo ? TL`AVs` : editor.format.isChampions ? TL`Points` : TL`EVs`}</th>
<th>{/* EV slider */}</th>
{!editor.isChampions && <th>{useIVs ? TL`IVs` : TL`DVs`}</th>}
{!editor.format.isChampions && <th>{useIVs ? TL`IVs` : TL`DVs`}</th>}
<th>{/* Final stat */}</th>
</tr>
{stats.map(([statID, statName, stat]) => <tr>
@@ -3820,7 +3780,7 @@ class StatForm extends preact.Component<{
onPointerDown={this.startEVSliderDrag}
onInput={this.changeEV} onChange={this.changeEV}
/></td>
{!editor.isChampions && <td><input
{!editor.format.isChampions && <td><input
name={`iv-${statID}`} min={0} max={useIVs ? 31 : 15} placeholder={`${defaultIVs[statID]}`}
style={narrow ? "width:22px" : "width:40px"} type={narrow ? 'text' : 'number'} inputMode="numeric"
class="textbox default-placeholder stat-input" onInput={this.changeIV}
@@ -3987,10 +3947,10 @@ class DetailsForm extends preact.Component<{
onInput={this.changeNickname} onChange={this.changeNickname}
/></label></p>
<p><label class="label">{TL.label(TL`Level`)}<input
name="level" value={set.level ?? ''} placeholder={`${editor.defaultLevel}`}
name="level" value={set.level ?? ''} placeholder={`${editor.format.defaultLevel}`}
type="number" inputMode="numeric" min="1" max="100" step="1"
class="textbox inputform numform default-placeholder" style="width: 50px"
onInput={this.changeLevel} onChange={this.changeLevel} disabled={editor.isChampions}
onInput={this.changeLevel} onChange={this.changeLevel} disabled={editor.format.isChampions}
/></label><small>(You probably want to change the team's levels by changing the format, not here)</small></p>
{editor.gen > 1 && (<>
<p><div class="label">{TL.label(TL`Shiny`)}<div class="labeled">
@@ -4017,14 +3977,14 @@ class DetailsForm extends preact.Component<{
/> Random</label>
</div>
)}</div></p>
{editor.isLetsGo ? (
{editor.format.isLetsGo ? (
<p><label class="label">{TL.label(TL`Happiness`)}<input
name="happiness" value="" placeholder="70"
type="number" inputMode="numeric"
class="textbox inputform numform default-placeholder" style="width: 50px"
onInput={this.changeHappiness} onChange={this.changeHappiness}
/></label></p>
) : (editor.gen < 8 || editor.isNatDex) && (
) : (editor.gen < 8 || editor.format.isNatDex) && (
<p><label class="label">{TL.label(TL`Happiness`)}<input
name="happiness" value={set.happiness ?? ''} placeholder="255"
type="number" inputMode="numeric" min="0" max="255" step="1"
@@ -4034,7 +3994,7 @@ class DetailsForm extends preact.Component<{
)}
</>
)}
{editor.gen === 8 && !editor.isBDSP && !species.cannotDynamax && (
{editor.gen === 8 && !editor.format.isBDSP && !species.cannotDynamax && (
<p>
<label class="label" style="display:inline">{TL.label(TL`Dynamax Level`)}<input
name="dynamaxlevel" value={set.dynamaxLevel ?? ''} placeholder="10"
@@ -4053,7 +4013,7 @@ class DetailsForm extends preact.Component<{
)}
</p>
)}
{((!editor.isLetsGo && editor.gen === 7) || editor.isNatDex || species.baseSpecies === 'Unown') && <p>
{((!editor.format.isLetsGo && editor.gen === 7) || editor.format.isNatDex || species.baseSpecies === 'Unown') && <p>
<label class="label">{TL.label(TL`Hidden Power Type`)}<select
name="hptype" class="select" onChange={this.changeHPType} value={editor.getHPType(set)}
>
@@ -4064,10 +4024,10 @@ class DetailsForm extends preact.Component<{
))}
</select></label>
</p>}
{editor.gen === 9 && !editor.isChampions && <p>
{editor.gen === 9 && !editor.format.isChampions && <p>
<label class="label" title={TL`Tera Type`}>
{TL.label(TL`Tera Type`)}{}
{species.requiredTeraType && editor.formeLegality === 'normal' ? (
{species.requiredTeraType && editor.format.formeLegality === 'normal' ? (
<button name="teratype" class="button cur" disabled>
<PSIcon type={species.requiredTeraType} new tera />
</button>

View File

@@ -367,7 +367,7 @@ export class Pokemon implements PokemonDetails, PokemonHealth {
if (ppUsed[1] < 0) ppUsed[1] = 0;
const move = this.side.battle.dex.moves.get(entry[0]);
let maxpp = (move.pp === 1 || move.noPPBoosts ? move.pp : move.pp * 8 / 5);
if (this.side.battle.tier.includes('Champions')) {
if (this.side.battle.format.isChampions) {
maxpp = move.pp > 20 ? 20 : move.pp;
maxpp = move.pp === 1 || move.noPPBoosts ? move.pp : (move.pp / 5 + 1) * 4;
}
@@ -957,7 +957,7 @@ export class Side {
}
pokemon.statusData.toxicTurns = 0;
if (this.battle.gen === 5) pokemon.statusData.sleepTurns = 0;
if (this.battle.tier.includes('Champions')) {
if (this.battle.format.isChampions) {
pokemon.timesAttacked = 0;
}
this.lastPokemon = pokemon;
@@ -1151,6 +1151,7 @@ export class Battle {
teamPreviewCount = 0;
speciesClause = false;
tier = '';
format = Dex.formats.get('');
gameType: 'singles' | 'doubles' | 'triples' | 'multi' | 'freeforall' | 'rotation' = 'singles';
compatMode = true;
rated: string | boolean = false;
@@ -3527,6 +3528,7 @@ export class Battle {
}
case 'tier': {
this.tier = args[1];
this.format = Dex.formats.get(this.tier);
if (this.tier.endsWith('Random Battle')) {
this.speciesClause = true;
}
@@ -3534,13 +3536,13 @@ export class Battle {
this.messageFadeTime = 40;
this.isBlitz = true;
}
if (this.tier.includes(`Let's Go`)) {
if (this.format.isLetsGo) {
this.dex = Dex.mod('gen7letsgo' as ID);
}
if (this.tier.includes('Super Staff Bros')) {
this.dex = Dex.mod('gen9ssb' as ID);
}
if (this.tier.includes(`Champions`)) {
if (this.format.isChampions) {
this.dex = Dex.mod('champions' as ID);
}
this.log(args);

View File

@@ -27,7 +27,7 @@ type BattleTextData = {
declare const BattleText: { [lang: string]: BattleTextData };
type BattleUITextEntry = string | null | { [context: string]: string | null };
declare const BattleUIText: { [lang: string]: { [english: string]: BattleUITextEntry } };
declare const BattleFormats: { [id: string]: import('./panel-teamdropdown').FormatData };
declare const BattleFormats: { [id: string]: import('./battle-dex-data').FormatData };
declare const BattlePokedex: { [id: string]: AnyObject };
declare const BattleMovedex: { [id: string]: AnyObject };
declare const BattleAbilities: { [id: string]: AnyObject };

View File

@@ -1480,6 +1480,7 @@ function toId() {
}
}
}
window.BattleFormats = Dex.formats.load(BattleFormats);
if (columnChanged) app.supports['formatColumns'] = true;
this.trigger('init:formats');
},

View File

@@ -761,9 +761,10 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
const moveRequest = choices.currentMoveRequest()!;
const canDynamax = moveRequest.canDynamax && !choices.alreadyMax;
const canMegaEvo = moveRequest.canMegaEvo && !choices.alreadyMega;
const canMegaEvoX = moveRequest.canMegaEvoX && !choices.alreadyMega;
const canMegaEvoY = moveRequest.canMegaEvoY && !choices.alreadyMega;
const alreadyMega = choices.alreadyMega && !this.props.room.battle.format.allowMultipleMegas;
const canMegaEvo = moveRequest.canMegaEvo && !alreadyMega;
const canMegaEvoX = moveRequest.canMegaEvoX && !alreadyMega;
const canMegaEvoY = moveRequest.canMegaEvoY && !alreadyMega;
const canZMove = moveRequest.zMoves && !choices.alreadyZ;
const canUltraBurst = moveRequest.canUltraBurst;
const canTerastallize = moveRequest.canTerastallize;

View File

@@ -366,6 +366,7 @@ export class MainMenuRoom extends PSRoom {
}
}
}
window.BattleFormats = Dex.formats.load(BattleFormats);
PS.teams.update('format');
}
handlePM(user1: string, user2: string, message?: string) {

View File

@@ -8,6 +8,7 @@
import { PS, type Team } from "./client-main";
import { PSIcon, PSPanelWrapper, PSRoomPanel } from "./panels";
import { Dex, TL, toID, type ID } from "./battle-dex";
import { type FormatData } from "./battle-dex-data";
import { Teams } from "./battle-teams";
export class PSTeambuilder {
@@ -358,29 +359,6 @@ class TeamDropdownPanel extends PSRoomPanel {
}
}
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';
}
declare const BattleFormats: { [id: string]: FormatData };
export type SelectType = 'teambuilder' | 'challenge' | 'search' | 'tournament';
class FormatDropdownPanel extends PSRoomPanel {
static readonly id = 'formatdropdown';