From d3d834c91a27751fd263da34daba23f1fa5cc8a3 Mon Sep 17 00:00:00 2001
From: Karthik Bandagonda <32044378+Karthik99999@users.noreply.github.com>
Date: Tue, 1 Jul 2025 03:20:57 -0400
Subject: [PATCH] Allow Picked Team Size to be used without Team Preview
(#11224)
* Add Pick Team rule
* Make Team Preview and Pick Team mutually exclusive
* Automatically apply rule if Picked Team Size exists without preview
* Remove mutually exclusive part
* Move teamsize to start action
---
config/formats.ts | 4 +--
data/mods/gen4/rulesets.ts | 2 +-
data/mods/gen8linked/scripts.ts | 41 +++++++++++++++++++++++++++--
data/mods/gen9ssb/scripts.ts | 5 ++--
data/mods/mixandmega/scripts.ts | 31 +++++++++++++++-------
data/mods/monkeyspaw/scripts.ts | 5 ++--
data/mods/randomroulette/scripts.ts | 26 ++++++++++++------
data/rulesets.ts | 7 +----
sim/battle.ts | 23 +++++++++++-----
9 files changed, 105 insertions(+), 39 deletions(-)
diff --git a/config/formats.ts b/config/formats.ts
index bde10d12e5..ab522b0d30 100644
--- a/config/formats.ts
+++ b/config/formats.ts
@@ -4653,7 +4653,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [
mod: 'gen4',
gameType: 'doubles',
searchShow: false,
- ruleset: ['Flat Rules', 'Min Team Size = 4', 'Max Team Size = 4', 'Limit Two Restricted'],
+ ruleset: ['Flat Rules', 'Limit Two Restricted'],
restricted: ['Restricted Legendary'],
banlist: ['Soul Dew'],
},
@@ -4662,7 +4662,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [
mod: 'gen4pt',
gameType: 'doubles',
searchShow: false,
- ruleset: ['Flat Rules', '! Adjust Level Down', 'Max Level = 50', 'Min Team Size = 4', 'Max Team Size = 4'],
+ ruleset: ['Flat Rules', '! Adjust Level Down', 'Max Level = 50'],
banlist: ['Tyranitar', 'Rotom', 'Judgment', 'Soul Dew'],
},
{
diff --git a/data/mods/gen4/rulesets.ts b/data/mods/gen4/rulesets.ts
index 68bdc86226..bbcb4232d2 100644
--- a/data/mods/gen4/rulesets.ts
+++ b/data/mods/gen4/rulesets.ts
@@ -5,7 +5,7 @@ export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable
},
flatrules: {
inherit: true,
- ruleset: ['Obtainable', 'Species Clause', 'Nickname Clause', 'Item Clause = 1', 'Adjust Level Down = 50', 'Cancel Mod'],
+ ruleset: ['Obtainable', 'Species Clause', 'Nickname Clause', 'Item Clause = 1', 'Adjust Level Down = 50', 'Picked Team Size = Auto', 'Cancel Mod'],
},
teampreview: {
inherit: true,
diff --git a/data/mods/gen8linked/scripts.ts b/data/mods/gen8linked/scripts.ts
index b952d274ee..1c4d7811cf 100644
--- a/data/mods/gen8linked/scripts.ts
+++ b/data/mods/gen8linked/scripts.ts
@@ -65,15 +65,52 @@ export const Scripts: ModdedBattleScriptsData = {
case 'start': {
for (const side of this.sides) {
if (side.pokemonLeft) side.pokemonLeft = side.pokemon.length;
+ this.add('teamsize', side.id, side.pokemon.length);
}
this.add('start');
- if (this.format.onBattleStart) this.format.onBattleStart.call(this);
+ // Change Zacian/Zamazenta into their Crowned formes
+ for (const pokemon of this.getAllPokemon()) {
+ let rawSpecies: Species | null = null;
+ if (pokemon.species.id === 'zacian' && pokemon.item === 'rustedsword') {
+ rawSpecies = this.dex.species.get('Zacian-Crowned');
+ } else if (pokemon.species.id === 'zamazenta' && pokemon.item === 'rustedshield') {
+ rawSpecies = this.dex.species.get('Zamazenta-Crowned');
+ }
+ if (!rawSpecies) continue;
+ const species = pokemon.setSpecies(rawSpecies);
+ if (!species) continue;
+ pokemon.baseSpecies = rawSpecies;
+ pokemon.details = pokemon.getUpdatedDetails();
+ // pokemon.setAbility(species.abilities['0'], null, true);
+ // pokemon.baseAbility = pokemon.ability;
+
+ const behemothMove: { [k: string]: string } = {
+ 'Zacian-Crowned': 'behemothblade', 'Zamazenta-Crowned': 'behemothbash',
+ };
+ const ironHead = pokemon.baseMoves.indexOf('ironhead');
+ if (ironHead >= 0) {
+ const move = this.dex.moves.get(behemothMove[rawSpecies.name]);
+ pokemon.baseMoveSlots[ironHead] = {
+ move: move.name,
+ id: move.id,
+ pp: move.noPPBoosts ? move.pp : move.pp * 8 / 5,
+ maxpp: move.noPPBoosts ? move.pp : move.pp * 8 / 5,
+ target: move.target,
+ disabled: false,
+ disabledSource: '',
+ used: false,
+ };
+ pokemon.moveSlots = pokemon.baseMoveSlots.slice();
+ }
+ }
+
+ this.format.onBattleStart?.call(this);
for (const rule of this.ruleTable.keys()) {
if ('+*-!'.includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
- if (subFormat.onBattleStart) subFormat.onBattleStart.call(this);
+ subFormat.onBattleStart?.call(this);
}
for (const side of this.sides) {
diff --git a/data/mods/gen9ssb/scripts.ts b/data/mods/gen9ssb/scripts.ts
index 62ee0a19d4..9a11f8846a 100644
--- a/data/mods/gen9ssb/scripts.ts
+++ b/data/mods/gen9ssb/scripts.ts
@@ -369,6 +369,7 @@ export const Scripts: ModdedBattleScriptsData = {
case 'start': {
for (const side of this.sides) {
if (side.pokemonLeft) side.pokemonLeft = side.pokemon.length;
+ this.add('teamsize', side.id, side.pokemon.length);
}
this.add('start');
@@ -409,11 +410,11 @@ export const Scripts: ModdedBattleScriptsData = {
}
}
- if (this.format.onBattleStart) this.format.onBattleStart.call(this);
+ this.format.onBattleStart?.call(this);
for (const rule of this.ruleTable.keys()) {
if ('+*-!'.includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
- if (subFormat.onBattleStart) subFormat.onBattleStart.call(this);
+ subFormat.onBattleStart?.call(this);
}
for (const side of this.sides) {
diff --git a/data/mods/mixandmega/scripts.ts b/data/mods/mixandmega/scripts.ts
index 431575013e..e2ca07864c 100644
--- a/data/mods/mixandmega/scripts.ts
+++ b/data/mods/mixandmega/scripts.ts
@@ -42,10 +42,6 @@ export const Scripts: ModdedBattleScriptsData = {
}
}
- for (const side of this.sides) {
- this.add('teamsize', side.id, side.pokemon.length);
- }
-
this.add('gen', this.gen);
this.add('tier', format.name);
@@ -54,11 +50,11 @@ export const Scripts: ModdedBattleScriptsData = {
this.add('rated', typeof this.rated === 'string' ? this.rated : '');
}
- if (format.onBegin) format.onBegin.call(this);
+ format.onBegin?.call(this);
for (const rule of this.ruleTable.keys()) {
if ('+*-!'.includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
- if (subFormat.onBegin) subFormat.onBegin.call(this);
+ subFormat.onBegin?.call(this);
}
for (const pokemon of this.getAllPokemon()) {
const item = pokemon.getItem();
@@ -87,11 +83,25 @@ export const Scripts: ModdedBattleScriptsData = {
this.add(`raw|
${format.customRules.length} custom rule${plural}:
${format.customRules.join(', ')}
`);
}
- if (format.onTeamPreview) format.onTeamPreview.call(this);
+ format.onTeamPreview?.call(this);
for (const rule of this.ruleTable.keys()) {
if ('+*-!'.includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
- if (subFormat.onTeamPreview) subFormat.onTeamPreview.call(this);
+ subFormat.onTeamPreview?.call(this);
+ }
+
+ if (this.requestState !== 'teampreview' && this.ruleTable.pickedTeamSize) {
+ this.add('clearpoke');
+ for (const side of this.sides) {
+ for (const pokemon of side.pokemon) {
+ // Still need to hide these formes since they change on battle start
+ const details = pokemon.details.replace(', shiny', '')
+ .replace(/(Zacian|Zamazenta)(?!-Crowned)/g, '$1-*')
+ .replace(/(Xerneas)(-[a-zA-Z?-]+)?/g, '$1-*');
+ this.addSplit(side.id, ['poke', pokemon.side.id, details, '']);
+ }
+ }
+ this.makeRequest('teampreview');
}
this.queue.addChoice({ choice: 'start' });
@@ -106,6 +116,7 @@ export const Scripts: ModdedBattleScriptsData = {
case 'start': {
for (const side of this.sides) {
if (side.pokemonLeft) side.pokemonLeft = side.pokemon.length;
+ this.add('teamsize', side.id, side.pokemon.length);
}
this.add('start');
@@ -147,11 +158,11 @@ export const Scripts: ModdedBattleScriptsData = {
}
}
- if (this.format.onBattleStart) this.format.onBattleStart.call(this);
+ this.format.onBattleStart?.call(this);
for (const rule of this.ruleTable.keys()) {
if ('+*-!'.includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
- if (subFormat.onBattleStart) subFormat.onBattleStart.call(this);
+ subFormat.onBattleStart?.call(this);
}
for (const side of this.sides) {
diff --git a/data/mods/monkeyspaw/scripts.ts b/data/mods/monkeyspaw/scripts.ts
index cf384d3623..dd9a604e34 100644
--- a/data/mods/monkeyspaw/scripts.ts
+++ b/data/mods/monkeyspaw/scripts.ts
@@ -188,6 +188,7 @@ export const Scripts: ModdedBattleScriptsData = {
case 'start': {
for (const side of this.sides) {
if (side.pokemonLeft) side.pokemonLeft = side.pokemon.length;
+ this.add('teamsize', side.id, side.pokemon.length);
}
this.add('start');
@@ -228,11 +229,11 @@ export const Scripts: ModdedBattleScriptsData = {
}
}
- if (this.format.onBattleStart) this.format.onBattleStart.call(this);
+ this.format.onBattleStart?.call(this);
for (const rule of this.ruleTable.keys()) {
if ('+*-!'.includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
- if (subFormat.onBattleStart) subFormat.onBattleStart.call(this);
+ subFormat.onBattleStart?.call(this);
}
for (const side of this.sides) {
diff --git a/data/mods/randomroulette/scripts.ts b/data/mods/randomroulette/scripts.ts
index a0ab26978c..9df29569da 100644
--- a/data/mods/randomroulette/scripts.ts
+++ b/data/mods/randomroulette/scripts.ts
@@ -80,10 +80,6 @@ export const Scripts: ModdedBattleScriptsData = {
}
}
- for (const side of this.sides) {
- this.add('teamsize', side.id, side.pokemon.length);
- }
-
this.add('gen', this.gen);
this.add('tier', format.name);
@@ -92,11 +88,11 @@ export const Scripts: ModdedBattleScriptsData = {
this.add('rated', typeof this.rated === 'string' ? this.rated : '');
}
- if (format.onBegin) format.onBegin.call(this);
+ format.onBegin?.call(this);
for (const rule of this.ruleTable.keys()) {
if ('+*-!'.includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
- if (subFormat.onBegin) subFormat.onBegin.call(this);
+ subFormat.onBegin?.call(this);
}
if (this.sides.some(side => !side.pokemon[0])) {
@@ -113,11 +109,25 @@ export const Scripts: ModdedBattleScriptsData = {
this.add(`raw|${format.customRules.length} custom rule${plural}:
${format.customRules.join(', ')}
`);
}
- if (format.onTeamPreview) format.onTeamPreview.call(this);
+ format.onTeamPreview?.call(this);
for (const rule of this.ruleTable.keys()) {
if ('+*-!'.includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
- if (subFormat.onTeamPreview) subFormat.onTeamPreview.call(this);
+ subFormat.onTeamPreview?.call(this);
+ }
+
+ if (this.requestState !== 'teampreview' && this.ruleTable.pickedTeamSize) {
+ this.add('clearpoke');
+ for (const side of this.sides) {
+ for (const pokemon of side.pokemon) {
+ // Still need to hide these formes since they change on battle start
+ const details = pokemon.details.replace(', shiny', '')
+ .replace(/(Zacian|Zamazenta)(?!-Crowned)/g, '$1-*')
+ .replace(/(Xerneas)(-[a-zA-Z?-]+)?/g, '$1-*');
+ this.addSplit(side.id, ['poke', pokemon.side.id, details, '']);
+ }
+ }
+ this.makeRequest('teampreview');
}
this.queue.addChoice({ choice: 'start' });
diff --git a/data/rulesets.ts b/data/rulesets.ts
index dd637762ea..acb6c1f924 100644
--- a/data/rulesets.ts
+++ b/data/rulesets.ts
@@ -1976,12 +1976,7 @@ export const Rulesets: import('../sim/dex-formats').FormatDataTable = {
name: 'Picked Team Size',
desc: "Team size (number of pokemon) that can be brought out of Team Preview",
hasValue: 'positive-integer',
- // hardcoded in sim/side
- onValidateRule() {
- if (!(this.ruleTable.has('teampreview') || this.ruleTable.has('teamtypepreview'))) {
- throw new Error(`The "Picked Team Size" rule${this.ruleTable.blame('pickedteamsize')} requires Team Preview.`);
- }
- },
+ // hardcoded in sim/side and sim/battle
},
minteamsize: {
effectType: 'ValidatorRule',
diff --git a/sim/battle.ts b/sim/battle.ts
index d0df521c08..f1799dc312 100644
--- a/sim/battle.ts
+++ b/sim/battle.ts
@@ -1868,10 +1868,6 @@ export class Battle {
}
}
- for (const side of this.sides) {
- this.add('teamsize', side.id, side.pokemon.length);
- }
-
this.add('gen', this.gen);
this.add('tier', format.name);
@@ -1908,6 +1904,20 @@ export class Battle {
subFormat.onTeamPreview?.call(this);
}
+ if (this.requestState !== 'teampreview' && this.ruleTable.pickedTeamSize) {
+ this.add('clearpoke');
+ for (const side of this.sides) {
+ for (const pokemon of side.pokemon) {
+ // Still need to hide these formes since they change on battle start
+ const details = pokemon.details.replace(', shiny', '')
+ .replace(/(Zacian|Zamazenta)(?!-Crowned)/g, '$1-*')
+ .replace(/(Xerneas)(-[a-zA-Z?-]+)?/g, '$1-*');
+ this.addSplit(side.id, ['poke', pokemon.side.id, details, '']);
+ }
+ }
+ this.makeRequest('teampreview');
+ }
+
this.queue.addChoice({ choice: 'start' });
this.midTurn = true;
if (!this.requestState) this.turnLoop();
@@ -2601,6 +2611,7 @@ export class Battle {
case 'start': {
for (const side of this.sides) {
if (side.pokemonLeft) side.pokemonLeft = side.pokemon.length;
+ this.add('teamsize', side.id, side.pokemon.length);
}
this.add('start');
@@ -2641,11 +2652,11 @@ export class Battle {
}
}
- if (this.format.onBattleStart) this.format.onBattleStart.call(this);
+ this.format.onBattleStart?.call(this);
for (const rule of this.ruleTable.keys()) {
if ('+*-!'.includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
- if (subFormat.onBattleStart) subFormat.onBattleStart.call(this);
+ subFormat.onBattleStart?.call(this);
}
for (const side of this.sides) {