From c5e4827329cdacd055e01a81584069914c6ec5a4 Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Wed, 18 Dec 2024 12:56:21 -0600 Subject: [PATCH 01/23] Mirror Herb: Copy simultaneous boosts from Howl, etc. --- data/items.ts | 23 +++++++++----- sim/pokemon.ts | 2 ++ test/sim/items/mirrorherb.js | 59 ++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 test/sim/items/mirrorherb.js diff --git a/data/items.ts b/data/items.ts index 99d707c02b1b..c4402f553827 100644 --- a/data/items.ts +++ b/data/items.ts @@ -3800,21 +3800,30 @@ export const Items: import('../sim/dex-items').ItemDataTable = { fling: { basePower: 30, }, + onStart(pokemon) { + this.effectState.started = true; + }, onFoeAfterBoost(boost, target, source, effect) { + if (!this.started) return; if (effect?.name === 'Opportunist' || effect?.name === 'Mirror Herb') return; - const boostPlus: SparseBoostsTable = {}; - let statsRaised = false; + if (!this.effectState.boosts) this.effectState.boosts = {} as SparseBoostsTable; + const boostPlus = this.effectState.boosts; let i: BoostID; for (i in boost) { if (boost[i]! > 0) { - boostPlus[i] = boost[i]; - statsRaised = true; + boostPlus[i] = (boostPlus[i] || 0) + boost[i]; + this.effectState.ready = true; } } - if (!statsRaised) return; - const pokemon: Pokemon = this.effectState.target; + }, + onUpdate(pokemon) { + if (!this.effectState.ready || !this.effectState.boosts) return; pokemon.useItem(); - this.boost(boostPlus, pokemon); + this.boost(this.effectState.boosts, pokemon); + }, + onEnd() { + delete this.effectState.boosts; + delete this.effectState.ready; }, num: 1883, gen: 9, diff --git a/sim/pokemon.ts b/sim/pokemon.ts index dbffa6995071..a53d4e7d011c 100644 --- a/sim/pokemon.ts +++ b/sim/pokemon.ts @@ -1494,6 +1494,8 @@ export class Pokemon { this.volatileStaleness = undefined; + this.itemState.started = false; + this.setSpecies(this.baseSpecies); } diff --git a/test/sim/items/mirrorherb.js b/test/sim/items/mirrorherb.js new file mode 100644 index 000000000000..1994062eb66a --- /dev/null +++ b/test/sim/items/mirrorherb.js @@ -0,0 +1,59 @@ +'use strict'; + +const assert = require('./../../assert'); +const common = require('./../../common'); + +let battle; + +describe("Mirror Herb", () => { + afterEach(() => battle.destroy()); + + it("should copy Anger Point", () => { + battle = common.createBattle([[ + {species: 'Snorlax', item: 'Mirror Herb', moves: ['stormthrow']}, + ], [ + {species: 'Primeape', ability: 'Anger Point', moves: ['sleeptalk']}, + ]]); + battle.makeChoices(); + assert.statStage(battle.p1.active[0], 'atk', 6); + }); + + it("should only copy the effective boost after the +6 cap", () => { + battle = common.createBattle({gameType: 'doubles'}, [[ + {species: 'Snorlax', item: 'Mirror Herb', moves: ['sleeptalk']}, + {species: 'Froslass', ability: 'Snow Cloak', moves: ['frostbreath']}, + ], [ + {species: 'Primeape', ability: 'Anger Point', moves: ['sleeptalk']}, + {species: 'Gyarados', ability: 'Intimidate', moves: ['sleeptalk']}, + ]]); + battle.makeChoices(); + assert.statStage(battle.p1.active[0], 'atk', -1 + 6); + }); + + it("should copy all 'simultaneous' boosts from multiple opponents", () => { + battle = common.createBattle({gameType: 'doubles'}, [[ + {species: 'Electrode', ability: 'No Guard', item: 'Mirror Herb', moves: ['recycle']}, + {species: 'Gyarados', ability: 'Intimidate', item: 'Wide Lens', moves: ['sleeptalk', 'air cutter']}, + ], [ + {species: 'Primeape', ability: 'Defiant', item: 'Weakness Policy', moves: ['sleeptalk', 'haze']}, + {species: 'Annihilape', ability: 'Defiant', item: 'Weakness Policy', moves: ['sleeptalk', 'howl']}, + ]]); + assert.statStage(battle.p1.active[0], 'atk', 4); + battle.makeChoices('auto', 'move haze, move howl'); + assert.statStage(battle.p1.active[0], 'atk', 2); + battle.makeChoices('move recycle, move air cutter', 'auto'); + assert.statStage(battle.p1.active[0], 'spa', 4); + }); + + it.skip("should wait for most entrance abilities before copying all their (opposing) boosts", () => { + battle = common.createBattle({gameType: 'doubles'}, [[ + {species: 'Electrode', item: 'Mirror Herb', moves: ['recycle']}, + {species: 'Gyarados', ability: 'Intimidate', moves: ['sleeptalk']}, + ], [ + {species: 'Zacian', ability: 'Intrepid Sword', moves: ['sleeptalk']}, + {species: 'Annihilape', ability: 'Defiant', moves: ['sleeptalk']}, + ]]); + common.saveReplay(battle); + assert.statStage(battle.p1.active[0], 'atk', 3); + }); +}); From d95b679ea78a32d37e85b4f94da23076027f99c6 Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Wed, 18 Dec 2024 13:32:59 -0600 Subject: [PATCH 02/23] Actually send End single events for Items on switch out --- sim/battle-actions.ts | 1 + sim/battle.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts index 22c2873ed52c..9a6b865c71f3 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -102,6 +102,7 @@ export class BattleActions { oldActive.illusion = null; this.battle.singleEvent('End', oldActive.getAbility(), oldActive.abilityState, oldActive); + this.battle.singleEvent('End', oldActive.getItem(), oldActive.itemState, oldActive); // if a pokemon is forced out by Whirlwind/etc or Eject Button/Pack, it can't use its chosen move this.battle.queue.cancelAction(oldActive); diff --git a/sim/battle.ts b/sim/battle.ts index a2d276ef5c30..5f2ea0586401 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -2372,6 +2372,7 @@ export class Battle { if (pokemon.side.totalFainted < 100) pokemon.side.totalFainted++; this.runEvent('Faint', pokemon, faintData.source, faintData.effect); this.singleEvent('End', pokemon.getAbility(), pokemon.abilityState, pokemon); + this.singleEvent('End', pokemon.getItem(), pokemon.itemState, pokemon); pokemon.clearVolatile(false); pokemon.fainted = true; pokemon.illusion = null; From d2a78a16407dc6f1a0773d4a1715157f4f3c8168 Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Thu, 26 Dec 2024 17:07:36 -0600 Subject: [PATCH 03/23] Overhaul 'SwitchIn' event for more accurate effect resolution order --- config/formats.ts | 16 ++-- data/abilities.ts | 122 +++++++++++++----------- data/conditions.ts | 4 +- data/items.ts | 56 +++++++---- data/mods/gen1/moves.ts | 8 +- data/mods/gen1/scripts.ts | 2 +- data/mods/gen1stadium/moves.ts | 6 +- data/mods/gen1stadium/scripts.ts | 2 +- data/mods/gen3/abilities.ts | 1 - data/mods/gen3/moves.ts | 2 +- data/mods/gen4/abilities.ts | 2 +- data/mods/gen4/moves.ts | 30 +++++- data/mods/gen6/moves.ts | 2 +- data/mods/gen7pokebilities/abilities.ts | 2 +- data/mods/gen7pokebilities/scripts.ts | 2 +- data/mods/gen8/moves.ts | 2 +- data/mods/gen8linked/moves.ts | 6 +- data/mods/gen8linked/scripts.ts | 17 +--- data/mods/gen9ssb/abilities.ts | 17 +--- data/mods/gen9ssb/conditions.ts | 8 +- data/mods/gen9ssb/moves.ts | 14 +-- data/mods/gen9ssb/scripts.ts | 21 ++-- data/mods/littlecolosseum/moves.ts | 4 +- data/mods/mixandmega/items.ts | 66 ++++++------- data/mods/mixandmega/scripts.ts | 12 +-- data/mods/partnersincrime/abilities.ts | 4 +- data/mods/partnersincrime/moves.ts | 9 +- data/mods/partnersincrime/scripts.ts | 51 +--------- data/mods/passiveaggressive/moves.ts | 8 +- data/mods/pokebilities/abilities.ts | 4 +- data/mods/pokebilities/scripts.ts | 2 +- data/mods/pokemoves/abilities.ts | 4 +- data/mods/sharedpower/abilities.ts | 4 +- data/mods/sharingiscaring/items.ts | 4 +- data/mods/sharingiscaring/scripts.ts | 8 +- data/moves.ts | 32 ++++--- data/random-battles/gen7/teams.ts | 4 +- server/chat-plugins/othermetas.ts | 2 +- sim/battle-actions.ts | 27 +++--- sim/battle.ts | 114 +++++++++++++++------- sim/dex-abilities.ts | 9 +- sim/dex-conditions.ts | 14 ++- sim/dex-items.ts | 13 ++- sim/field.ts | 18 ++-- sim/pokemon.ts | 64 ++++++++----- sim/side.ts | 9 +- sim/state.ts | 6 +- test/sim/abilities/commander.js | 4 +- test/sim/abilities/iceface.js | 2 +- test/sim/abilities/neutralizinggas.js | 1 + test/sim/abilities/steelyspirit.js | 6 +- test/sim/items/mirrorherb.js | 10 +- test/sim/items/seeds.js | 7 +- test/sim/items/whiteherb.js | 2 +- test/sim/misc/partnersincrime.js | 27 ++++++ test/sim/moves/tarshot.js | 2 +- 56 files changed, 501 insertions(+), 394 deletions(-) create mode 100644 test/sim/misc/partnersincrime.js diff --git a/config/formats.ts b/config/formats.ts index b3ce64fd1bc1..b142f513b685 100644 --- a/config/formats.ts +++ b/config/formats.ts @@ -17,6 +17,8 @@ New sections will be added to the bottom of the specified column. The column value will be ignored for repeat sections. */ +import {EffectState} from '../sim/pokemon'; + export const Formats: import('../sim/dex-formats').FormatList = [ // S/V Singles @@ -638,7 +640,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ const itemTable = new Set(); for (const set of team) { const item = this.dex.items.get(set.item); - if (!item.megaStone && !item.onPrimal && !item.forcedForme?.endsWith('Origin') && + if (!item.megaStone && !item.isPrimalOrb && !item.forcedForme?.endsWith('Origin') && !item.name.startsWith('Rusted') && !item.name.endsWith('Mask')) continue; const natdex = this.ruleTable.has('standardnatdex'); if (natdex && item.id !== 'ultranecroziumz') continue; @@ -646,7 +648,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ if (species.isNonstandard && !this.ruleTable.has(`+pokemontag:${this.toID(species.isNonstandard)}`)) { return [`${species.baseSpecies} does not exist in gen 9.`]; } - if ((item.itemUser?.includes(species.name) && !item.megaStone && !item.onPrimal) || + if ((item.itemUser?.includes(species.name) && !item.megaStone && !item.isPrimalOrb) || (natdex && species.name.startsWith('Necrozma-') && item.id === 'ultranecroziumz')) { continue; } @@ -728,7 +730,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ if (!format.getSharedPower) format = this.dex.formats.get('gen9sharedpower'); for (const ability of format.getSharedPower!(pokemon)) { const effect = 'ability:' + ability; - pokemon.volatiles[effect] = {id: this.toID(effect), target: pokemon}; + pokemon.volatiles[effect] = new EffectState({id: this.toID(effect), target: pokemon}, this); if (!pokemon.m.abils) pokemon.m.abils = []; if (!pokemon.m.abils.includes(effect)) pokemon.m.abils.push(effect); } @@ -1462,14 +1464,14 @@ export const Formats: import('../sim/dex-formats').FormatList = [ if (!pokemon.m.innate && !BAD_ABILITIES.includes(this.toID(ally.ability))) { pokemon.m.innate = 'ability:' + ally.ability; if (!ngas || ally.getAbility().flags['cantsuppress'] || pokemon.hasItem('Ability Shield')) { - pokemon.volatiles[pokemon.m.innate] = {id: pokemon.m.innate, target: pokemon}; + pokemon.volatiles[pokemon.m.innate] = new EffectState({id: pokemon.m.innate, target: pokemon}, this); pokemon.m.startVolatile = true; } } if (!ally.m.innate && !BAD_ABILITIES.includes(this.toID(pokemon.ability))) { ally.m.innate = 'ability:' + pokemon.ability; if (!ngas || pokemon.getAbility().flags['cantsuppress'] || ally.hasItem('Ability Shield')) { - ally.volatiles[ally.m.innate] = {id: ally.m.innate, target: ally}; + ally.volatiles[ally.m.innate] = new EffectState({id: ally.m.innate, target: ally}, this); ally.m.startVolatile = true; } } @@ -1767,7 +1769,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ for (const item of format.getSharedItems!(pokemon)) { if (pokemon.m.sharedItemsUsed.includes(item)) continue; const effect = 'item:' + item; - pokemon.volatiles[effect] = {id: this.toID(effect), target: pokemon}; + pokemon.volatiles[effect] = new EffectState({id: this.toID(effect), target: pokemon}, this); if (!pokemon.m.items) pokemon.m.items = []; if (!pokemon.m.items.includes(effect)) pokemon.m.items.push(effect); } @@ -2585,7 +2587,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ if (!format.getSharedPower) format = this.dex.formats.get('gen9sharedpower'); for (const ability of format.getSharedPower!(pokemon)) { const effect = 'ability:' + ability; - pokemon.volatiles[effect] = {id: this.toID(effect), target: pokemon}; + pokemon.volatiles[effect] = new EffectState({id: this.toID(effect), target: pokemon}, this); if (!pokemon.m.abils) pokemon.m.abils = []; if (!pokemon.m.abils.includes(effect)) pokemon.m.abils.push(effect); } diff --git a/data/abilities.ts b/data/abilities.ts index 46747db16e53..3f6518dc5ab1 100644 --- a/data/abilities.ts +++ b/data/abilities.ts @@ -89,15 +89,12 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { }, airlock: { onSwitchIn(pokemon) { - this.effectState.switchingIn = true; + // Air Lock does not activate when Skill Swapped or when Neutralizing Gas leaves the field + this.add('-ability', pokemon, 'Air Lock'); + this.singleEvent('Start', this.effect, this.effectState, pokemon); }, onStart(pokemon) { - // Air Lock does not activate when Skill Swapped or when Neutralizing Gas leaves the field pokemon.abilityState.ending = false; // Clear the ending flag - if (this.effectState.switchingIn) { - this.add('-ability', pokemon, 'Air Lock'); - this.effectState.switchingIn = false; - } this.eachEvent('WeatherChange', this.effect); }, onEnd(pokemon) { @@ -255,11 +252,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 165, }, asoneglastrier: { - onPreStart(pokemon) { - this.add('-ability', pokemon, 'As One'); - this.add('-ability', pokemon, 'Unnerve'); - this.effectState.unnerved = true; - }, + onSwitchInPriority: 1, onStart(pokemon) { if (this.effectState.unnerved) return; this.add('-ability', pokemon, 'As One'); @@ -283,11 +276,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 266, }, asonespectrier: { - onPreStart(pokemon) { - this.add('-ability', pokemon, 'As One'); - this.add('-ability', pokemon, 'Unnerve'); - this.effectState.unnerved = true; - }, + onSwitchInPriority: 1, onStart(pokemon) { if (this.effectState.unnerved) return; this.add('-ability', pokemon, 'As One'); @@ -547,15 +536,12 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { }, cloudnine: { onSwitchIn(pokemon) { - this.effectState.switchingIn = true; + // Cloud Nine does not activate when Skill Swapped or when Neutralizing Gas leaves the field + this.add('-ability', pokemon, 'Cloud Nine'); + this.singleEvent('Start', this.effect, this.effectState, pokemon); }, onStart(pokemon) { - // Cloud Nine does not activate when Skill Swapped or when Neutralizing Gas leaves the field pokemon.abilityState.ending = false; // Clear the ending flag - if (this.effectState.switchingIn) { - this.add('-ability', pokemon, 'Cloud Nine'); - this.effectState.switchingIn = false; - } this.eachEvent('WeatherChange', this.effect); }, onEnd(pokemon) { @@ -610,8 +596,13 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 213, }, commander: { + onSwitchInPriority: -2, + onStart(target) { + this.effectState.started = true; + this.singleEvent('Update', this.effect, this.effectState, target); + }, onUpdate(pokemon) { - if (this.gameType !== 'doubles') return; + if (this.gameType !== 'doubles' || !this.effectState.started) return; const ally = pokemon.allies()[0]; if (!ally || pokemon.baseSpecies.baseSpecies !== 'Tatsugiri' || ally.baseSpecies.baseSpecies !== 'Dondozo') { // Handle any edge cases @@ -693,6 +684,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 212, }, costar: { + onSwitchInPriority: -2, onStart(pokemon) { const ally = pokemon.allies()[0]; if (!ally) return; @@ -1055,10 +1047,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { }, drizzle: { onStart(source) { - for (const action of this.queue) { - if (action.choice === 'runPrimal' && action.pokemon === source && source.species.id === 'kyogre') return; - if (action.choice !== 'runSwitch' && action.choice !== 'runPrimal') break; - } + if (source.species.id === 'kyogre' && source.item === 'blueorb') return; this.field.setWeather('raindance'); }, flags: {}, @@ -1068,10 +1057,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { }, drought: { onStart(source) { - for (const action of this.queue) { - if (action.choice === 'runPrimal' && action.pokemon === source && source.species.id === 'groudon') return; - if (action.choice !== 'runSwitch' && action.choice !== 'runPrimal') break; - } + if (source.species.id === 'groudon' && source.item === 'redorb') return; this.field.setWeather('sunnyday'); }, flags: {}, @@ -1329,6 +1315,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 18, }, flowergift: { + onSwitchInPriority: -2, onStart(pokemon) { this.singleEvent('WeatherChange', this.effect, this.effectState, pokemon); }, @@ -1416,6 +1403,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 218, }, forecast: { + onSwitchInPriority: -2, onStart(pokemon) { this.singleEvent('WeatherChange', this.effect, this.effectState, pokemon); }, @@ -1819,6 +1807,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 118, }, hospitality: { + onSwitchInPriority: -2, onStart(pokemon) { for (const ally of pokemon.adjacentAllies()) { this.heal(ally.baseMaxhp / 4, ally, pokemon); @@ -1913,6 +1902,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 115, }, iceface: { + onSwitchInPriority: -2, onStart(pokemon) { if (this.field.isWeather(['hail', 'snow']) && pokemon.species.id === 'eiscuenoice') { this.add('-activate', pokemon, 'ability: Ice Face'); @@ -2060,19 +2050,14 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { }, imposter: { onSwitchIn(pokemon) { - this.effectState.switchingIn = true; - }, - onStart(pokemon) { // Imposter does not activate when Skill Swapped or when Neutralizing Gas leaves the field - if (!this.effectState.switchingIn) return; - // copies across in doubles/triples + // Imposter copies across in doubles/triples // (also copies across in multibattle and diagonally in free-for-all, // but side.foe already takes care of those) const target = pokemon.side.foe.active[pokemon.side.foe.active.length - 1 - pokemon.position]; if (target) { pokemon.transformInto(target, this.dex.abilities.get('imposter')); } - this.effectState.switchingIn = false; }, flags: {failroleplay: 1, noreceiver: 1, noentrain: 1, notrace: 1}, name: "Imposter", @@ -2512,6 +2497,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 196, }, mimicry: { + onSwitchInPriority: -1, onStart(pokemon) { this.singleEvent('TerrainChange', this.effect, this.effectState, pokemon); }, @@ -2839,7 +2825,8 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { }, neutralizinggas: { // Ability suppression implemented in sim/pokemon.ts:Pokemon#ignoringAbility - onPreStart(pokemon) { + onSwitchInPriority: 2, + onSwitchIn(pokemon) { this.add('-ability', pokemon, 'Neutralizing Gas'); pokemon.abilityState.ending = false; const strongWeathers = ['desolateland', 'primordialsea', 'deltastream']; @@ -2975,16 +2962,34 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { opportunist: { onFoeAfterBoost(boost, target, source, effect) { if (effect?.name === 'Opportunist' || effect?.name === 'Mirror Herb') return; - const pokemon = this.effectState.target; - const positiveBoosts: Partial = {}; + if (!this.effectState.boosts) this.effectState.boosts = {} as SparseBoostsTable; + const boostPlus = this.effectState.boosts; let i: BoostID; for (i in boost) { if (boost[i]! > 0) { - positiveBoosts[i] = boost[i]; + boostPlus[i] = (boostPlus[i] || 0) + boost[i]; } } - if (Object.keys(positiveBoosts).length < 1) return; - this.boost(positiveBoosts, pokemon); + }, + onAnySwitchInPriority: -3, + onAnySwitchIn() { + if (!this.effectState.boosts) return; + this.boost(this.effectState.boosts, this.effectState.target); + delete this.effectState.boosts; + }, + onAnyAfterMove() { + if (!this.effectState.boosts) return; + this.boost(this.effectState.boosts, this.effectState.target); + delete this.effectState.boosts; + }, + onResidualOrder: 29, + onResidual(pokemon) { + if (!this.effectState.boosts) return; + this.boost(this.effectState.boosts, this.effectState.target); + delete this.effectState.boosts; + }, + onEnd() { + delete this.effectState.boosts; }, flags: {}, name: "Opportunist", @@ -3420,6 +3425,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 168, }, protosynthesis: { + onSwitchInPriority: -2, onStart(pokemon) { this.singleEvent('WeatherChange', this.effect, this.effectState, pokemon); }, @@ -3556,6 +3562,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 272, }, quarkdrive: { + onSwitchInPriority: -2, onStart(pokemon) { this.singleEvent('TerrainChange', this.effect, this.effectState, pokemon); }, @@ -3951,6 +3958,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 157, }, schooling: { + onSwitchInPriority: -1, onStart(pokemon) { if (pokemon.baseSpecies.baseSpecies !== 'Wishiwashi' || pokemon.level < 20 || pokemon.transformed) return; if (pokemon.hp > pokemon.maxhp / 4) { @@ -4145,6 +4153,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 19, }, shieldsdown: { + onSwitchInPriority: -1, onStart(pokemon) { if (pokemon.baseSpecies.baseSpecies !== 'Minior' || pokemon.transformed) return; if (pokemon.hp > pokemon.maxhp / 2) { @@ -4232,7 +4241,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { }, onResidual(pokemon) { if (!pokemon.activeTurns) { - this.effectState.duration += 1; + this.effectState.duration! += 1; } }, onModifyAtkPriority: 5, @@ -4872,7 +4881,8 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 308, }, terashift: { - onPreStart(pokemon) { + onSwitchInPriority: 2, + onSwitchIn(pokemon) { if (pokemon.baseSpecies.baseSpecies !== 'Terapagos') return; if (pokemon.species.forme !== 'Terastal') { this.add('-activate', pokemon, 'ability: Tera Shift'); @@ -5033,19 +5043,23 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { }, trace: { onStart(pokemon) { + this.effectState.seek = true; // n.b. only affects Hackmons // interaction with No Ability is complicated: https://www.smogon.com/forums/threads/pokemon-sun-moon-battle-mechanics-research.3586701/page-76#post-7790209 if (pokemon.adjacentFoes().some(foeActive => foeActive.ability === 'noability')) { - this.effectState.gaveUp = true; + this.effectState.seek = false; } // interaction with Ability Shield is similar to No Ability if (pokemon.hasItem('Ability Shield')) { this.add('-block', pokemon, 'item: Ability Shield'); - this.effectState.gaveUp = true; + this.effectState.seek = false; + } + if (this.effectState.seek) { + this.singleEvent('Update', this.effect, this.effectState, pokemon); } }, onUpdate(pokemon) { - if (!pokemon.isStarted || this.effectState.gaveUp) return; + if (!this.effectState.seek) return; const possibleTargets = pokemon.adjacentFoes().filter( target => !target.getAbility().flags['notrace'] && target.ability !== 'noability' @@ -5170,10 +5184,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 84, }, unnerve: { - onPreStart(pokemon) { - this.add('-ability', pokemon, 'Unnerve'); - this.effectState.unnerved = true; - }, + onSwitchInPriority: 1, onStart(pokemon) { if (this.effectState.unnerved) return; this.add('-ability', pokemon, 'Unnerve'); @@ -5559,12 +5570,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { pokemon.formeChange('Palafin-Hero', this.effect, true); } }, - onSwitchIn() { - this.effectState.switchingIn = true; - }, - onStart(pokemon) { - if (!this.effectState.switchingIn) return; - this.effectState.switchingIn = false; + onSwitchIn(pokemon) { if (pokemon.baseSpecies.baseSpecies !== 'Palafin') return; if (!this.effectState.heroMessageDisplayed && pokemon.species.forme === 'Hero') { this.add('-activate', pokemon, 'ability: Zero to Hero'); diff --git a/data/conditions.ts b/data/conditions.ts index 5c0f040ac391..0f4e73353ffa 100644 --- a/data/conditions.ts +++ b/data/conditions.ts @@ -382,9 +382,9 @@ export const Conditions: import('../sim/dex-conditions').ConditionDataTable = { } }, onResidualOrder: 3, - onResidual(side: any) { + onResidual(target: Pokemon) { if (this.getOverflowedTurnCount() < this.effectState.endingTurn) return; - side.removeSlotCondition(this.getAtSlot(this.effectState.targetSlot), 'futuremove'); + target.side.removeSlotCondition(this.getAtSlot(this.effectState.targetSlot), 'futuremove'); }, onEnd(target) { const data = this.effectState; diff --git a/data/items.ts b/data/items.ts index c4402f553827..27f685cc2e2d 100644 --- a/data/items.ts +++ b/data/items.ts @@ -193,7 +193,7 @@ export const Items: import('../sim/dex-items').ItemDataTable = { onDamagingHit(damage, target, source, move) { this.add('-enditem', target, 'Air Balloon'); target.item = ''; - target.itemState = {id: '', target}; + target.itemState.clear(); this.runEvent('AfterUseItem', target, null, null, this.dex.items.get('airballoon')); }, onAfterSubDamage(damage, target, source, effect) { @@ -201,7 +201,7 @@ export const Items: import('../sim/dex-items').ItemDataTable = { if (effect.effectType === 'Move') { this.add('-enditem', target, 'Air Balloon'); target.item = ''; - target.itemState = {id: '', target}; + target.itemState.clear(); this.runEvent('AfterUseItem', target, null, null, this.dex.items.get('airballoon')); } }, @@ -568,19 +568,18 @@ export const Items: import('../sim/dex-items').ItemDataTable = { blueorb: { name: "Blue Orb", spritenum: 41, + onSwitchInPriority: -1, onSwitchIn(pokemon) { - if (pokemon.isActive && pokemon.baseSpecies.name === 'Kyogre') { - this.queue.insertChoice({choice: 'runPrimal', pokemon: pokemon}); + if (pokemon.isActive && pokemon.baseSpecies.name === 'Kyogre' && !pokemon.transformed) { + pokemon.formeChange('Kyogre-Primal', this.effect, true); } }, - onPrimal(pokemon) { - pokemon.formeChange('Kyogre-Primal', this.effect, true); - }, onTakeItem(item, source) { if (source.baseSpecies.baseSpecies === 'Kyogre') return false; return true; }, itemUser: ["Kyogre"], + isPrimalOrb: true, num: 535, gen: 6, isNonstandard: "Past", @@ -614,12 +613,13 @@ export const Items: import('../sim/dex-items').ItemDataTable = { fling: { basePower: 30, }, - onStart() { + onSwitchInPriority: -2, + onStart(pokemon) { this.effectState.started = true; + this.singleEvent('Update', this.effect, this.effectState, pokemon); }, onUpdate(pokemon) { if (!this.effectState.started || pokemon.transformed) return; - if (this.queue.peek(true)?.choice === 'runSwitch') return; if (pokemon.hasAbility('protosynthesis') && !this.field.isWeather('sunnyday') && pokemon.useItem()) { pokemon.addVolatile('protosynthesis'); @@ -1639,6 +1639,7 @@ export const Items: import('../sim/dex-items').ItemDataTable = { fling: { basePower: 10, }, + onSwitchInPriority: -1, onStart(pokemon) { if (!pokemon.ignoringItem() && this.field.isTerrain('electricterrain')) { pokemon.useItem(); @@ -2323,6 +2324,7 @@ export const Items: import('../sim/dex-items').ItemDataTable = { fling: { basePower: 10, }, + onSwitchInPriority: -1, onStart(pokemon) { if (!pokemon.ignoringItem() && this.field.isTerrain('grassyterrain')) { pokemon.useItem(); @@ -3800,11 +3802,7 @@ export const Items: import('../sim/dex-items').ItemDataTable = { fling: { basePower: 30, }, - onStart(pokemon) { - this.effectState.started = true; - }, onFoeAfterBoost(boost, target, source, effect) { - if (!this.started) return; if (effect?.name === 'Opportunist' || effect?.name === 'Mirror Herb') return; if (!this.effectState.boosts) this.effectState.boosts = {} as SparseBoostsTable; const boostPlus = this.effectState.boosts; @@ -3816,9 +3814,21 @@ export const Items: import('../sim/dex-items').ItemDataTable = { } } }, - onUpdate(pokemon) { + onAnySwitchInPriority: -3, + onAnySwitchIn() { + if (!this.effectState.ready || !this.effectState.boosts) return; + (this.effectState.target as Pokemon).useItem(); + }, + onAnyAfterMove() { if (!this.effectState.ready || !this.effectState.boosts) return; - pokemon.useItem(); + (this.effectState.target as Pokemon).useItem(); + }, + onResidualOrder: 29, + onResidual(pokemon) { + if (!this.effectState.ready || !this.effectState.boosts) return; + (this.effectState.target as Pokemon).useItem(); + }, + onUse(pokemon) { this.boost(this.effectState.boosts, pokemon); }, onEnd() { @@ -3834,6 +3844,7 @@ export const Items: import('../sim/dex-items').ItemDataTable = { fling: { basePower: 10, }, + onSwitchInPriority: -1, onStart(pokemon) { if (!pokemon.ignoringItem() && this.field.isTerrain('mistyterrain')) { pokemon.useItem(); @@ -4531,6 +4542,7 @@ export const Items: import('../sim/dex-items').ItemDataTable = { fling: { basePower: 10, }, + onSwitchInPriority: -1, onStart(pokemon) { if (!pokemon.ignoringItem() && this.field.isTerrain('psychicterrain')) { pokemon.useItem(); @@ -4756,19 +4768,18 @@ export const Items: import('../sim/dex-items').ItemDataTable = { redorb: { name: "Red Orb", spritenum: 390, + onSwitchInPriority: -1, onSwitchIn(pokemon) { - if (pokemon.isActive && pokemon.baseSpecies.name === 'Groudon') { - this.queue.insertChoice({choice: 'runPrimal', pokemon: pokemon}); + if (pokemon.isActive && pokemon.baseSpecies.name === 'Groudon' && !pokemon.transformed) { + pokemon.formeChange('Groudon-Primal', this.effect, true); } }, - onPrimal(pokemon) { - pokemon.formeChange('Groudon-Primal', this.effect, true); - }, onTakeItem(item, source) { if (source.baseSpecies.baseSpecies === 'Groudon') return false; return true; }, itemUser: ["Groudon"], + isPrimalOrb: true, num: 534, gen: 6, isNonstandard: "Past", @@ -4902,6 +4913,7 @@ export const Items: import('../sim/dex-items').ItemDataTable = { fling: { basePower: 100, }, + onSwitchInPriority: -1, onStart(pokemon) { if (!pokemon.ignoringItem() && this.field.getPseudoWeather('trickroom')) { pokemon.useItem(); @@ -7179,6 +7191,10 @@ export const Items: import('../sim/dex-items').ItemDataTable = { } }, }, + onSwitchInPriority: -2, + onStart(pokemon) { + this.singleEvent('Update', this.effect, this.effectState, pokemon); + }, onUpdate(pokemon) { let activate = false; const boosts: SparseBoostsTable = {}; diff --git a/data/mods/gen1/moves.ts b/data/mods/gen1/moves.ts index 38565a0bc0b8..cfa7d6e58531 100644 --- a/data/mods/gen1/moves.ts +++ b/data/mods/gen1/moves.ts @@ -94,7 +94,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { * about to end its partial trapping. **/ if (target.volatiles['partiallytrapped']) { - if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration > 1) { + if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration! > 1) { target.volatiles['partiallytrapped'].duration = 2; } } @@ -155,7 +155,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { * about to end its partial trapping. **/ if (target.volatiles['partiallytrapped']) { - if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration > 1) { + if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration! > 1) { target.volatiles['partiallytrapped'].duration = 2; } } @@ -338,7 +338,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { * about to end its partial trapping. **/ if (target.volatiles['partiallytrapped']) { - if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration > 1) { + if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration! > 1) { target.volatiles['partiallytrapped'].duration = 2; } } @@ -968,7 +968,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { * about to end its partial trapping. **/ if (target.volatiles['partiallytrapped']) { - if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration > 1) { + if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration! > 1) { target.volatiles['partiallytrapped'].duration = 2; } } diff --git a/data/mods/gen1/scripts.ts b/data/mods/gen1/scripts.ts index 21f1d9d67a81..5617c4443093 100644 --- a/data/mods/gen1/scripts.ts +++ b/data/mods/gen1/scripts.ts @@ -528,7 +528,7 @@ export const Scripts: ModdedBattleScriptsData = { // Handle here the applying of partial trapping moves to Pokémon with Substitute if (targetSub && moveData.volatileStatus && moveData.volatileStatus === 'partiallytrapped') { target.addVolatile(moveData.volatileStatus, pokemon, move); - if (!pokemon.volatiles['partialtrappinglock'] || pokemon.volatiles['partialtrappinglock'].duration > 1) { + if (!pokemon.volatiles['partialtrappinglock'] || pokemon.volatiles['partialtrappinglock'].duration! > 1) { target.volatiles[moveData.volatileStatus].duration = 2; } } diff --git a/data/mods/gen1stadium/moves.ts b/data/mods/gen1stadium/moves.ts index a7bb04d3170b..3837e7d898c1 100644 --- a/data/mods/gen1stadium/moves.ts +++ b/data/mods/gen1stadium/moves.ts @@ -33,14 +33,14 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { onAfterSetStatus(status, pokemon) { // Sleep, freeze, and partial trap will just pause duration. if (pokemon.volatiles['flinch']) { - this.effectState.duration++; + this.effectState.duration!++; } else if (pokemon.volatiles['partiallytrapped']) { - this.effectState.duration++; + this.effectState.duration!++; } else { switch (status.id) { case 'slp': case 'frz': - this.effectState.duration++; + this.effectState.duration!++; break; } } diff --git a/data/mods/gen1stadium/scripts.ts b/data/mods/gen1stadium/scripts.ts index 90a0b055ef18..711946942ddb 100644 --- a/data/mods/gen1stadium/scripts.ts +++ b/data/mods/gen1stadium/scripts.ts @@ -391,7 +391,7 @@ export const Scripts: ModdedBattleScriptsData = { const targetHadSub = !!target.volatiles['substitute']; if (targetHadSub && moveData.volatileStatus && moveData.volatileStatus === 'partiallytrapped') { target.addVolatile(moveData.volatileStatus, pokemon, move); - if (!pokemon.volatiles['partialtrappinglock'] || pokemon.volatiles['partialtrappinglock'].duration > 1) { + if (!pokemon.volatiles['partialtrappinglock'] || pokemon.volatiles['partialtrappinglock'].duration! > 1) { target.volatiles[moveData.volatileStatus].duration = 2; } } diff --git a/data/mods/gen3/abilities.ts b/data/mods/gen3/abilities.ts index 3d2bd5b5e6f7..969632d3ff24 100644 --- a/data/mods/gen3/abilities.ts +++ b/data/mods/gen3/abilities.ts @@ -178,7 +178,6 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa inherit: true, onUpdate() {}, onStart(pokemon) { - if (!pokemon.isStarted) return; const target = pokemon.side.randomFoe(); if (!target || target.fainted) return; const ability = target.getAbility(); diff --git a/data/mods/gen3/moves.ts b/data/mods/gen3/moves.ts index 84efa1dbb19e..a0798ea47abd 100644 --- a/data/mods/gen3/moves.ts +++ b/data/mods/gen3/moves.ts @@ -209,7 +209,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { noCopy: true, onStart(pokemon) { if (!this.queue.willMove(pokemon)) { - this.effectState.duration++; + this.effectState.duration!++; } if (!pokemon.lastMove) { return false; diff --git a/data/mods/gen4/abilities.ts b/data/mods/gen4/abilities.ts index 2e71d1a2eab9..44ecd00958fb 100644 --- a/data/mods/gen4/abilities.ts +++ b/data/mods/gen4/abilities.ts @@ -526,7 +526,7 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa trace: { inherit: true, onUpdate(pokemon) { - if (!pokemon.isStarted) return; + if (!this.effectState.seek) return; const target = pokemon.side.randomFoe(); if (!target || target.fainted) return; const ability = target.getAbility(); diff --git a/data/mods/gen4/moves.ts b/data/mods/gen4/moves.ts index 821b6ea66463..f044c5fbca94 100644 --- a/data/mods/gen4/moves.ts +++ b/data/mods/gen4/moves.ts @@ -326,7 +326,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { noCopy: true, onStart(pokemon) { if (!this.queue.willMove(pokemon)) { - this.effectState.duration++; + this.effectState.duration!++; } if (!pokemon.lastMove) { return false; @@ -1553,6 +1553,23 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { spikes: { inherit: true, flags: {metronome: 1, mustpressure: 1}, + condition: { + // this is a side condition + onSideStart(side) { + this.add('-sidestart', side, 'Spikes'); + this.effectState.layers = 1; + }, + onSideRestart(side) { + if (this.effectState.layers >= 3) return false; + this.add('-sidestart', side, 'Spikes'); + this.effectState.layers++; + }, + onEntryHazard(pokemon) { + if (!pokemon.isGrounded() || pokemon.hasItem('heavydutyboots')) return; + const damageAmounts = [0, 3, 4, 6]; // 1/8, 1/6, 1/4 + this.damage(damageAmounts[this.effectState.layers] * pokemon.maxhp / 24); + }, + }, }, spite: { inherit: true, @@ -1561,6 +1578,17 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { stealthrock: { inherit: true, flags: {metronome: 1, mustpressure: 1}, + condition: { + // this is a side condition + onSideStart(side) { + this.add('-sidestart', side, 'move: Stealth Rock'); + }, + onEntryHazard(pokemon) { + if (pokemon.hasItem('heavydutyboots')) return; + const typeMod = this.clampIntRange(pokemon.runEffectiveness(this.dex.getActiveMove('stealthrock')), -6, 6); + this.damage(pokemon.maxhp * Math.pow(2, typeMod) / 8); + }, + }, }, struggle: { inherit: true, diff --git a/data/mods/gen6/moves.ts b/data/mods/gen6/moves.ts index 01a560af818d..b1f6f389e6bc 100644 --- a/data/mods/gen6/moves.ts +++ b/data/mods/gen6/moves.ts @@ -50,7 +50,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { this.effectState.move = target.lastMove.id; this.add('-start', target, 'Encore'); if (!this.queue.willMove(target)) { - this.effectState.duration++; + this.effectState.duration!++; } }, onOverrideAction(pokemon, target, move) { diff --git a/data/mods/gen7pokebilities/abilities.ts b/data/mods/gen7pokebilities/abilities.ts index edffea067633..2c2a17f34bfa 100644 --- a/data/mods/gen7pokebilities/abilities.ts +++ b/data/mods/gen7pokebilities/abilities.ts @@ -82,7 +82,7 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa trace: { inherit: true, onUpdate(pokemon) { - if (!pokemon.isStarted) return; + if (!this.effectState.seek) return; const isAbility = pokemon.ability === 'trace'; const possibleTargets: Pokemon[] = []; for (const target of pokemon.side.foe.active) { diff --git a/data/mods/gen7pokebilities/scripts.ts b/data/mods/gen7pokebilities/scripts.ts index b0199e4bad6b..1938144b1163 100644 --- a/data/mods/gen7pokebilities/scripts.ts +++ b/data/mods/gen7pokebilities/scripts.ts @@ -177,7 +177,7 @@ export const Scripts: ModdedBattleScriptsData = { if (source.zMove) { this.battle.add('-burst', this, apparentSpecies, species.requiredItem); this.moveThisTurnResult = true; // Ultra Burst counts as an action for Truant - } else if (source.onPrimal) { + } else if (source.isPrimalOrb) { if (this.illusion) { this.ability = ''; this.battle.add('-primal', this.illusion); diff --git a/data/mods/gen8/moves.ts b/data/mods/gen8/moves.ts index 2a22dafba506..c75c911025f6 100644 --- a/data/mods/gen8/moves.ts +++ b/data/mods/gen8/moves.ts @@ -529,7 +529,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { onSideStart(side) { this.add('-sidestart', side, 'move: Sticky Web'); }, - onEntryHazard(pokemon) { + onSwitchIn(pokemon) { if (!pokemon.isGrounded() || pokemon.hasItem('heavydutyboots')) return; this.add('-activate', pokemon, 'move: Sticky Web'); this.boost({spe: -1}, pokemon, this.effectState.source, this.dex.getActiveMove('stickyweb')); diff --git a/data/mods/gen8linked/moves.ts b/data/mods/gen8linked/moves.ts index db0878bba43a..0a2265dfde3e 100644 --- a/data/mods/gen8linked/moves.ts +++ b/data/mods/gen8linked/moves.ts @@ -43,7 +43,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { this.add('-fail', source); return null; } - if (target.volatiles.mustrecharge && target.volatiles.mustrecharge.duration < 2) { + if (target.volatiles.mustrecharge && target.volatiles.mustrecharge.duration! < 2) { // Duration may not be lower than 2 if Sucker Punch is used as a low-priority move // i.e. if Sucker Punch is linked with a negative priority move this.attrLastMove('[still]'); @@ -160,7 +160,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { this.queue.willMove(pokemon) || (pokemon === this.activePokemon && this.activeMove && !this.activeMove.isExternal) ) { - this.effectState.duration--; + this.effectState.duration!--; } if (!lastMove) { this.debug('pokemon hasn\'t moved yet'); @@ -235,7 +235,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { this.effectState.move = linkedMoves; } if (!this.queue.willMove(target)) { - this.effectState.duration++; + this.effectState.duration!++; } }, onOverrideAction(pokemon, target, move) { diff --git a/data/mods/gen8linked/scripts.ts b/data/mods/gen8linked/scripts.ts index ed19a3000e13..d83234099d19 100644 --- a/data/mods/gen8linked/scripts.ts +++ b/data/mods/gen8linked/scripts.ts @@ -173,17 +173,9 @@ export const Scripts: ModdedBattleScriptsData = { } } break; - case 'runUnnerve': - this.singleEvent('PreStart', action.pokemon.getAbility(), action.pokemon.abilityState, action.pokemon); - break; case 'runSwitch': this.actions.runSwitch(action.pokemon); break; - case 'runPrimal': - if (!action.pokemon.transformed) { - this.singleEvent('Primal', action.pokemon.getItem(), action.pokemon.itemState, action.pokemon); - } - break; case 'shift': if (!action.pokemon.isActive) return false; if (action.pokemon.fainted) return false; @@ -198,7 +190,7 @@ export const Scripts: ModdedBattleScriptsData = { this.clearActiveMove(true); this.updateSpeed(); residualPokemon = this.getAllActive().map(pokemon => [pokemon, pokemon.getUndynamaxedHP()] as const); - this.residualEvent('Residual'); + this.fieldEvent('Residual'); this.add('upkeep'); break; } @@ -416,9 +408,8 @@ export const Scripts: ModdedBattleScriptsData = { // Note that the speed stat used is after any volatile replacements like Speed Swap, // but before any multipliers like Agility or Choice Scarf // Ties go to whichever Pokemon has had the ability for the least amount of time - dancers.sort( - (a, b) => -(b.storedStats['spe'] - a.storedStats['spe']) || b.abilityOrder - a.abilityOrder - ); + dancers.sort((a, b) => -(b.storedStats['spe'] - a.storedStats['spe']) || + b.abilityState.effectOrder - a.abilityState.effectOrder); for (const dancer of dancers) { if (this.battle.faintMessages()) break; if (dancer.fainted) continue; @@ -451,7 +442,7 @@ export const Scripts: ModdedBattleScriptsData = { runUnnerve: 100, runSwitch: 101, - runPrimal: 102, + // runPrimal: 102, (deprecated) switch: 103, megaEvo: 104, runDynamax: 105, diff --git a/data/mods/gen9ssb/abilities.ts b/data/mods/gen9ssb/abilities.ts index d04c4ad59399..c7117038709a 100644 --- a/data/mods/gen9ssb/abilities.ts +++ b/data/mods/gen9ssb/abilities.ts @@ -510,10 +510,7 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa shortDesc: "Drizzle + Static.", name: "Astrothunder", onStart(source) { - for (const action of this.queue) { - if (action.choice === 'runPrimal' && action.pokemon === source && source.species.id === 'kyogre') return; - if (action.choice !== 'runSwitch' && action.choice !== 'runPrimal') break; - } + if (source.species.id === 'kyogre' && source.item === 'blueorb') return; this.field.setWeather('raindance'); }, onDamagingHit(damage, target, source, move) { @@ -1935,10 +1932,7 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa name: "Rainy's Aura", onStart(source) { if (this.suppressingAbility(source)) return; - for (const action of this.queue) { - if (action.choice === 'runPrimal' && action.pokemon === source && source.species.id === 'kyogre') return; - if (action.choice !== 'runSwitch' && action.choice !== 'runPrimal') break; - } + if (source.species.id === 'kyogre' && source.item === 'blueorb') return; this.field.setWeather('raindance'); }, onAnyBasePowerPriority: 20, @@ -2282,10 +2276,7 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa desc: "On switch-in, this Pokemon summons Sunny Day. If Sunny Day is active, this Pokemon's Speed is 1.5x.", name: "Ride the Sun!", onStart(source) { - for (const action of this.queue) { - if (action.choice === 'runPrimal' && action.pokemon === source && source.species.id === 'groudon') return; - if (action.choice !== 'runSwitch' && action.choice !== 'runPrimal') break; - } + if (source.species.id === 'groudon' && source.item === 'redorb') return; this.field.setWeather('sunnyday'); }, onModifySpe(spe, pokemon) { @@ -3078,7 +3069,7 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa }, neutralizinggas: { inherit: true, - onPreStart(pokemon) { + onSwitchIn(pokemon) { this.add('-ability', pokemon, 'Neutralizing Gas'); pokemon.abilityState.ending = false; for (const target of this.getAllActive()) { diff --git a/data/mods/gen9ssb/conditions.ts b/data/mods/gen9ssb/conditions.ts index 320f09d04b61..34127c4bb9ce 100644 --- a/data/mods/gen9ssb/conditions.ts +++ b/data/mods/gen9ssb/conditions.ts @@ -38,7 +38,7 @@ export const Conditions: {[id: IDEntry]: ModdedConditionData & {innateName?: str }, }, aegiibpmsg: { - onSwap(target, source) { + onSwitchIn(target) { if (!target.fainted) { this.add(`c:|${getName('aegii')}|~yes ${target.name}`); target.side.removeSlotCondition(target, 'aegiibpmsg'); @@ -637,7 +637,7 @@ export const Conditions: {[id: IDEntry]: ModdedConditionData & {innateName?: str this.add(`c:|${getName('Clementine')}|I fucking love air-conditioning.`); } }, - onFoeSwitchIn(pokemon) { + onAnySwitchIn(pokemon) { if ((pokemon.illusion || pokemon).name === 'Kennedy') { this.add(`c:|${getName('Clementine')}|yikes`); } @@ -1204,7 +1204,7 @@ export const Conditions: {[id: IDEntry]: ModdedConditionData & {innateName?: str onSwitchOut() { this.add(`c:|${getName('Kennedy')}|Stream some Taylor Swift whilst I'm gone!`); // TODO replace }, - onFoeSwitchIn(pokemon) { + onAnySwitchIn(pokemon) { switch ((pokemon.illusion || pokemon).name) { case 'Clementine': this.add(`c:|${getName('Kennedy')}|Not the Fr*nch....`); @@ -1802,7 +1802,7 @@ export const Conditions: {[id: IDEntry]: ModdedConditionData & {innateName?: str onFaint() { this.add(`c:|${getName('PartMan')}|Okay weeb`); }, - onFoeSwitchIn(pokemon) { + onAnySwitchIn(pokemon) { if (pokemon.name === 'Hydrostatics') { this.add(`c:|${getName('PartMan')}|LUAAAAA!`); this.add(`c:|${getName('PartMan')}|/me pats`); diff --git a/data/mods/gen9ssb/moves.ts b/data/mods/gen9ssb/moves.ts index cdbef5356fb5..7d47bbd34fe6 100644 --- a/data/mods/gen9ssb/moves.ts +++ b/data/mods/gen9ssb/moves.ts @@ -180,7 +180,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { }, slotCondition: 'freeswitchbutton', condition: { - onSwap(target) { + onSwitchIn(target) { if (!target.fainted && (target.hp < target.maxhp)) { target.heal(target.maxhp / 3); this.add('-heal', target, target.getHealth, '[from] move: Free Switch Button'); @@ -2513,7 +2513,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { target.removeVolatile('wonderwing'); }, onDamage(damage, target, source, effect) { - if (this.effectState.duration < 2) return; + if (this.effectState.duration! < 2) return; this.add('-activate', source, 'move: Wonder Wing'); return false; }, @@ -3947,7 +3947,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { slotCondition: 'qualitycontrolzoomies', }, condition: { - onSwap(target) { + onSwitchIn(target) { if (!target.fainted) { target.addVolatile('catstampofapproval'); target.side.removeSlotCondition(target, 'qualitycontrolzoomies'); @@ -4034,7 +4034,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { }, slotCondition: 'nyaa', condition: { - onSwap(target) { + onSwitchIn(target) { const source = this.effectState.source; if (!target.fainted) { this.add(`c:|${getName((source.illusion || source).name)}|~nyaa ${target.name}`); @@ -5984,7 +5984,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { onStart(pokemon, source) { this.effectState.hp = source.maxhp / 2; }, - onSwap(target) { + onSwitchIn(target) { if (!target.fainted) target.addVolatile('aquaring', target); }, onResidualOrder: 4, @@ -6263,7 +6263,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { }, slotCondition: 'tagyoureit', condition: { - onSwap(target) { + onSwitchIn(target) { if (target && !target.fainted) { this.add('-anim', target, "Baton Pass", target); target.addVolatile('focusenergy'); @@ -6845,7 +6845,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { onSideStart(side) { this.add('-sidestart', side, 'move: Sticky Web'); }, - onEntryHazard(pokemon) { + onSwitchIn(pokemon) { if (!pokemon.isGrounded() || pokemon.hasItem('heavydutyboots') || pokemon.hasAbility('eternalgenerator')) return; this.add('-activate', pokemon, 'move: Sticky Web'); this.boost({spe: -1}, pokemon, pokemon.side.foe.active[0], this.dex.getActiveMove('stickyweb')); diff --git a/data/mods/gen9ssb/scripts.ts b/data/mods/gen9ssb/scripts.ts index 3fe0e55a73b8..93ee14cb0e4d 100644 --- a/data/mods/gen9ssb/scripts.ts +++ b/data/mods/gen9ssb/scripts.ts @@ -549,17 +549,9 @@ export const Scripts: ModdedBattleScriptsData = { // @ts-ignore action.pokemon.side.removeSlotCondition(action.pokemon, 'scapegoat'); break; - case 'runUnnerve': - this.singleEvent('PreStart', action.pokemon.getAbility(), action.pokemon.abilityState, action.pokemon); - break; case 'runSwitch': this.actions.runSwitch(action.pokemon); break; - case 'runPrimal': - if (!action.pokemon.transformed) { - this.singleEvent('Primal', action.pokemon.getItem(), action.pokemon.itemState, action.pokemon); - } - break; case 'shift': if (!action.pokemon.isActive) return false; if (action.pokemon.fainted) return false; @@ -574,7 +566,7 @@ export const Scripts: ModdedBattleScriptsData = { this.clearActiveMove(true); this.updateSpeed(); residualPokemon = this.getAllActive().map(pokemon => [pokemon, pokemon.getUndynamaxedHP()] as const); - this.residualEvent('Residual'); + this.fieldEvent('Residual'); this.add('upkeep'); break; } @@ -618,7 +610,7 @@ export const Scripts: ModdedBattleScriptsData = { return false; } - if (this.gen >= 5) { + if (this.gen >= 5 && action.choice !== 'start') { this.eachEvent('Update'); for (const [pokemon, originalHP] of residualPokemon) { const maxhp = pokemon.getUndynamaxedHP(pokemon.maxhp); @@ -920,16 +912,15 @@ export const Scripts: ModdedBattleScriptsData = { } else { this.battle.add(isDrag ? 'drag' : 'switch', pokemon, pokemon.getDetails); } - pokemon.abilityOrder = this.battle.abilityOrder++; + pokemon.abilityState.effectOrder = this.battle.effectOrder++; + pokemon.itemState.effectOrder = this.battle.effectOrder++; if (isDrag && this.battle.gen === 2) pokemon.draggedIn = this.battle.turn; pokemon.previouslySwitchedIn++; if (isDrag && this.battle.gen >= 5) { // runSwitch happens immediately so that Mold Breaker can make hazards bypass Clear Body and Levitate - this.battle.singleEvent('PreStart', pokemon.getAbility(), pokemon.abilityState, pokemon); this.runSwitch(pokemon); } else { - this.battle.queue.insertChoice({choice: 'runUnnerve', pokemon}); this.battle.queue.insertChoice({choice: 'runSwitch', pokemon}); } @@ -1233,7 +1224,7 @@ export const Scripts: ModdedBattleScriptsData = { // but before any multipliers like Agility or Choice Scarf // Ties go to whichever Pokemon has had the ability for the least amount of time dancers.sort( - (a, b) => -(b.storedStats['spe'] - a.storedStats['spe']) || b.abilityOrder - a.abilityOrder + (a, b) => -(b.storedStats['spe'] - a.storedStats['spe']) || b.abilityState.effectOrder - a.abilityState.effectOrder ); const targetOf1stDance = this.battle.activeTarget!; for (const dancer of dancers) { @@ -2016,7 +2007,7 @@ export const Scripts: ModdedBattleScriptsData = { runUnnerve: 100, runSwitch: 101, - runPrimal: 102, + // runPrimal: 102, switch: 103, megaEvo: 104, runDynamax: 105, diff --git a/data/mods/littlecolosseum/moves.ts b/data/mods/littlecolosseum/moves.ts index 7cb2e7b9e4f5..bdcdf7809343 100644 --- a/data/mods/littlecolosseum/moves.ts +++ b/data/mods/littlecolosseum/moves.ts @@ -39,7 +39,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { onSideStart(side) { this.add('-sidestart', side, 'move: Stealth Rock'); }, - onEntryHazard(pokemon) { + onSwitchIn(pokemon) { if (pokemon.hasItem('heavydutyboots') || pokemon.hasAbility('hazardabsorb') || pokemon.hasAbility('hover')) return; const typeMod = this.clampIntRange(pokemon.runEffectiveness(this.dex.getActiveMove('stealthrock')), -6, 6); this.damage(pokemon.maxhp * Math.pow(2, typeMod) / 8); @@ -72,7 +72,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { this.add('-sidestart', side, 'Spikes'); this.effectState.layers++; }, - onEntryHazard(pokemon) { + onSwitchIn(pokemon) { if (!pokemon.isGrounded()) return; if (pokemon.hasItem('heavydutyboots') || pokemon.hasAbility('hazardabsorb')) return; const damageAmounts = [0, 3, 4, 6]; // 1/8, 1/6, 1/4 diff --git a/data/mods/mixandmega/items.ts b/data/mods/mixandmega/items.ts index e4d3533f2753..84000d8f0f40 100644 --- a/data/mods/mixandmega/items.ts +++ b/data/mods/mixandmega/items.ts @@ -59,21 +59,18 @@ export const Items: import('../../../sim/dex-items').ModdedItemDataTable = { blueorb: { inherit: true, onSwitchIn(pokemon) { - if (pokemon.isActive && !pokemon.species.isPrimal) { - this.queue.insertChoice({pokemon, choice: 'runPrimal'}); - } - }, - onPrimal(pokemon) { - // @ts-ignore - const species: Species = this.actions.getMixedSpecies(pokemon.m.originalSpecies, 'Kyogre-Primal', pokemon); - if (pokemon.m.originalSpecies === 'Kyogre') { - pokemon.formeChange(species, this.effect, true); - } else { - pokemon.formeChange(species, this.effect, true); - pokemon.baseSpecies = species; - this.add('-start', pokemon, 'Blue Orb', '[silent]'); + if (pokemon.isActive && !pokemon.species.isPrimal && !pokemon.transformed) { + // @ts-ignore + const species: Species = this.actions.getMixedSpecies(pokemon.m.originalSpecies, 'Kyogre-Primal', pokemon); + if (pokemon.m.originalSpecies === 'Kyogre') { + pokemon.formeChange(species, this.effect, true); + } else { + pokemon.formeChange(species, this.effect, true); + pokemon.baseSpecies = species; + this.add('-start', pokemon, 'Blue Orb', '[silent]'); + } + pokemon.canTerastallize = null; } - pokemon.canTerastallize = null; }, onTakeItem: false, isNonstandard: null, @@ -213,31 +210,28 @@ export const Items: import('../../../sim/dex-items').ModdedItemDataTable = { redorb: { inherit: true, onSwitchIn(pokemon) { - if (pokemon.isActive && !pokemon.species.isPrimal) { - this.queue.insertChoice({pokemon, choice: 'runPrimal'}); - } - }, - onPrimal(pokemon) { - // @ts-ignore - const species: Species = this.actions.getMixedSpecies(pokemon.m.originalSpecies, 'Groudon-Primal', pokemon); - if (pokemon.m.originalSpecies === 'Groudon') { - pokemon.formeChange(species, this.effect, true); - } else { - pokemon.formeChange(species, this.effect, true); - pokemon.baseSpecies = species; - this.add('-start', pokemon, 'Red Orb', '[silent]'); - const apparentSpecies = pokemon.illusion ? pokemon.illusion.species.name : pokemon.m.originalSpecies; - const oSpecies = this.dex.species.get(apparentSpecies); - if (pokemon.illusion) { - const types = oSpecies.types; - if (types.length > 1 || types[types.length - 1] !== 'Fire') { - this.add('-start', pokemon, 'typechange', (types[0] !== 'Fire' ? types[0] + '/' : '') + 'Fire', '[silent]'); + if (pokemon.isActive && !pokemon.species.isPrimal && !pokemon.transformed) { + // @ts-ignore + const species: Species = this.actions.getMixedSpecies(pokemon.m.originalSpecies, 'Groudon-Primal', pokemon); + if (pokemon.m.originalSpecies === 'Groudon') { + pokemon.formeChange(species, this.effect, true); + } else { + pokemon.formeChange(species, this.effect, true); + pokemon.baseSpecies = species; + this.add('-start', pokemon, 'Red Orb', '[silent]'); + const apparentSpecies = pokemon.illusion ? pokemon.illusion.species.name : pokemon.m.originalSpecies; + const oSpecies = this.dex.species.get(apparentSpecies); + if (pokemon.illusion) { + const types = oSpecies.types; + if (types.length > 1 || types[types.length - 1] !== 'Fire') { + this.add('-start', pokemon, 'typechange', (types[0] !== 'Fire' ? types[0] + '/' : '') + 'Fire', '[silent]'); + } + } else if (oSpecies.types.length !== pokemon.species.types.length || oSpecies.types[1] !== pokemon.species.types[1]) { + this.add('-start', pokemon, 'typechange', pokemon.species.types.join('/'), '[silent]'); } - } else if (oSpecies.types.length !== pokemon.species.types.length || oSpecies.types[1] !== pokemon.species.types[1]) { - this.add('-start', pokemon, 'typechange', pokemon.species.types.join('/'), '[silent]'); } + pokemon.canTerastallize = null; } - pokemon.canTerastallize = null; }, onTakeItem: false, isNonstandard: null, diff --git a/data/mods/mixandmega/scripts.ts b/data/mods/mixandmega/scripts.ts index a687f445ba51..71e0ccc02592 100644 --- a/data/mods/mixandmega/scripts.ts +++ b/data/mods/mixandmega/scripts.ts @@ -257,17 +257,9 @@ export const Scripts: ModdedBattleScriptsData = { this.add('-heal', action.target, action.target.getHealth, '[from] move: Revival Blessing'); action.pokemon.side.removeSlotCondition(action.pokemon, 'revivalblessing'); break; - case 'runUnnerve': - this.singleEvent('PreStart', action.pokemon.getAbility(), action.pokemon.abilityState, action.pokemon); - break; case 'runSwitch': this.actions.runSwitch(action.pokemon); break; - case 'runPrimal': - if (!action.pokemon.transformed) { - this.singleEvent('Primal', action.pokemon.getItem(), action.pokemon.itemState, action.pokemon); - } - break; case 'shift': if (!action.pokemon.isActive) return false; if (action.pokemon.fainted) return false; @@ -282,7 +274,7 @@ export const Scripts: ModdedBattleScriptsData = { this.clearActiveMove(true); this.updateSpeed(); residualPokemon = this.getAllActive().map(pokemon => [pokemon, pokemon.getUndynamaxedHP()] as const); - this.residualEvent('Residual'); + this.fieldEvent('Residual'); this.add('upkeep'); break; } @@ -326,7 +318,7 @@ export const Scripts: ModdedBattleScriptsData = { return false; } - if (this.gen >= 5) { + if (this.gen >= 5 && action.choice !== 'start') { this.eachEvent('Update'); for (const [pokemon, originalHP] of residualPokemon) { const maxhp = pokemon.getUndynamaxedHP(pokemon.maxhp); diff --git a/data/mods/partnersincrime/abilities.ts b/data/mods/partnersincrime/abilities.ts index 72b17c7736e2..71f407e0fa1d 100644 --- a/data/mods/partnersincrime/abilities.ts +++ b/data/mods/partnersincrime/abilities.ts @@ -2,7 +2,7 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa neutralizinggas: { inherit: true, // Ability suppression implemented in sim/pokemon.ts:Pokemon#ignoringAbility - onPreStart(pokemon) { + onSwitchIn(pokemon) { this.add('-ability', pokemon, 'Neutralizing Gas'); pokemon.abilityState.ending = false; // Remove setter's innates before the ability starts @@ -52,7 +52,7 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa trace: { inherit: true, onUpdate(pokemon) { - if (!pokemon.isStarted || this.effectState.gaveUp) return; + if (!this.effectState.seek) return; const isAbility = pokemon.ability === 'trace'; const possibleTargets = pokemon.adjacentFoes().filter( diff --git a/data/mods/partnersincrime/moves.ts b/data/mods/partnersincrime/moves.ts index aef86678d043..33393bdccf6b 100644 --- a/data/mods/partnersincrime/moves.ts +++ b/data/mods/partnersincrime/moves.ts @@ -1,3 +1,5 @@ +import {EffectState} from '../../../sim/pokemon'; + export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { gastroacid: { inherit: true, @@ -49,6 +51,9 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { lunardance: { inherit: true, condition: { + onSwitchIn(target) { + this.singleEvent('Swap', this.effect, this.effectState, target); + }, onSwap(target) { if ( !target.fainted && ( @@ -148,7 +153,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { } source.ability = targetAbility.id; - source.abilityState = {id: this.toID(source.ability), target: source}; + source.abilityState = new EffectState({id: this.toID(source.ability), target: source}, this); if (source.m.innate && source.m.innate.endsWith(targetAbility.id)) { source.removeVolatile(source.m.innate); delete source.m.innate; @@ -163,7 +168,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { } target.ability = sourceAbility.id; - target.abilityState = {id: this.toID(target.ability), target: target}; + target.abilityState = new EffectState({id: this.toID(target.ability), target: target}, this); if (target.m.innate && target.m.innate.endsWith(sourceAbility.id)) { target.removeVolatile(target.m.innate); delete target.m.innate; diff --git a/data/mods/partnersincrime/scripts.ts b/data/mods/partnersincrime/scripts.ts index 68f391110d89..9009eab8e35b 100644 --- a/data/mods/partnersincrime/scripts.ts +++ b/data/mods/partnersincrime/scripts.ts @@ -1,4 +1,5 @@ import {Utils} from '../../../lib'; +import {EffectState} from '../../../sim/pokemon'; export const Scripts: ModdedBattleScriptsData = { gen: 9, @@ -218,53 +219,6 @@ export const Scripts: ModdedBattleScriptsData = { this.makeRequest('move'); }, - actions: { - runSwitch(pokemon) { - this.battle.runEvent('Swap', pokemon); - - if (this.battle.gen >= 5) { - this.battle.runEvent('SwitchIn', pokemon); - } - - this.battle.runEvent('EntryHazard', pokemon); - - if (this.battle.gen <= 4) { - this.battle.runEvent('SwitchIn', pokemon); - } - - const ally = pokemon.side.active.find(mon => mon && mon !== pokemon && !mon.fainted); - - if (this.battle.gen <= 2 && !pokemon.side.faintedThisTurn && pokemon.draggedIn !== this.battle.turn) { - this.battle.runEvent('AfterSwitchInSelf', pokemon); - } - if (!pokemon.hp) return false; - pokemon.isStarted = true; - if (!pokemon.fainted) { - this.battle.singleEvent('Start', pokemon.getAbility(), pokemon.abilityState, pokemon); - // Start innates - let status; - if (pokemon.m.startVolatile && pokemon.m.innate) { - status = this.battle.dex.conditions.get(pokemon.m.innate); - this.battle.singleEvent('Start', status, pokemon.volatiles[status.id], pokemon); - pokemon.m.startVolatile = false; - } - if (ally && ally.m.startVolatile && ally.m.innate) { - status = this.battle.dex.conditions.get(ally.m.innate); - this.battle.singleEvent('Start', status, ally.volatiles[status.id], ally); - ally.m.startVolatile = false; - } - // pic end - this.battle.singleEvent('Start', pokemon.getItem(), pokemon.itemState, pokemon); - } - if (this.battle.gen === 4) { - for (const foeActive of pokemon.foes()) { - foeActive.removeVolatile('substitutebroken'); - } - } - pokemon.draggedIn = null; - return true; - }, - }, pokemon: { setAbility(ability, source, isFromFormeChange) { if (!this.hp) return false; @@ -286,7 +240,7 @@ export const Scripts: ModdedBattleScriptsData = { this.battle.dex.moves.get(this.battle.effect.id)); } this.ability = ability.id; - this.abilityState = {id: ability.id, target: this}; + this.abilityState = new EffectState({id: ability.id, target: this}, this.battle); if (ability.id && this.battle.gen > 3) { this.battle.singleEvent('Start', ability, this.abilityState, this, source); if (ally && ally.ability !== this.ability) { @@ -305,7 +259,6 @@ export const Scripts: ModdedBattleScriptsData = { this.removeVolatile(this.m.innate); delete this.m.innate; } - this.abilityOrder = this.battle.abilityOrder++; return oldAbility; }, hasAbility(ability) { diff --git a/data/mods/passiveaggressive/moves.ts b/data/mods/passiveaggressive/moves.ts index c7dcea0b7589..9043b7e84ccf 100644 --- a/data/mods/passiveaggressive/moves.ts +++ b/data/mods/passiveaggressive/moves.ts @@ -6,7 +6,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { onSideStart(side, source) { this.add('-sidestart', side, 'move: Stealth Rock'); }, - onEntryHazard(pokemon) { + onSwitchIn(pokemon) { const calc = calculate(this, this.effectState.source, pokemon, 'stealthrock'); if (pokemon.hasItem('heavydutyboots') || !calc) return; this.damage(calc * pokemon.maxhp / 8); @@ -20,7 +20,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { onSideStart(side, source) { this.add('-sidestart', side, 'move: G-Max Steelsurge'); }, - onEntryHazard(pokemon) { + onSwitchIn(pokemon) { const calc = calculate(this, this.effectState.source, pokemon, 'stealthrock'); if (pokemon.hasItem('heavydutyboots') || !calc) return; this.damage(calc * pokemon.maxhp / 8); @@ -40,7 +40,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { this.add('-sidestart', side, 'Spikes'); this.effectState.layers++; }, - onEntryHazard(pokemon) { + onSwitchIn(pokemon) { const calc = calculate(this, this.effectState.source, pokemon, 'spikes'); if (!calc || !pokemon.isGrounded() || pokemon.hasItem('heavydutyboots')) return; const damageAmounts = [0, 3, 4, 6]; // 1/8, 1/6, 1/4 @@ -279,7 +279,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { this.add('-sidestart', side, 'move: Toxic Spikes'); this.effectState.layers++; }, - onEntryHazard(pokemon) { + onSwitchIn(pokemon) { if (!pokemon.isGrounded()) return; if (pokemon.hasType('Poison')) { this.add('-sideend', pokemon.side, 'move: Toxic Spikes', '[of] ' + pokemon); diff --git a/data/mods/pokebilities/abilities.ts b/data/mods/pokebilities/abilities.ts index de3ba4e4d1eb..78e3e8408032 100644 --- a/data/mods/pokebilities/abilities.ts +++ b/data/mods/pokebilities/abilities.ts @@ -38,7 +38,7 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa neutralizinggas: { inherit: true, // Ability suppression implemented in sim/pokemon.ts:Pokemon#ignoringAbility - onPreStart(pokemon) { + onSwitchIn(pokemon) { this.add('-ability', pokemon, 'Neutralizing Gas'); pokemon.abilityState.ending = false; // Remove setter's innates before the ability starts @@ -139,7 +139,7 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa trace: { inherit: true, onUpdate(pokemon) { - if (!pokemon.isStarted) return; + if (!this.effectState.seek) return; const isAbility = pokemon.ability === 'trace'; const possibleTargets: Pokemon[] = []; for (const target of pokemon.side.foe.active) { diff --git a/data/mods/pokebilities/scripts.ts b/data/mods/pokebilities/scripts.ts index e9564991a2ac..990eb2f175a9 100644 --- a/data/mods/pokebilities/scripts.ts +++ b/data/mods/pokebilities/scripts.ts @@ -177,7 +177,7 @@ export const Scripts: ModdedBattleScriptsData = { if (source.zMove) { this.battle.add('-burst', this, apparentSpecies, species.requiredItem); this.moveThisTurnResult = true; // Ultra Burst counts as an action for Truant - } else if (source.onPrimal) { + } else if (source.isPrimalOrb) { if (this.illusion) { this.ability = ''; this.battle.add('-primal', this.illusion); diff --git a/data/mods/pokemoves/abilities.ts b/data/mods/pokemoves/abilities.ts index e448152e0e93..1860c91dcced 100644 --- a/data/mods/pokemoves/abilities.ts +++ b/data/mods/pokemoves/abilities.ts @@ -2,7 +2,7 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa trace: { inherit: true, onUpdate(pokemon) { - if (!pokemon.isStarted || this.effectState.gaveUp) return; + if (!this.effectState.seek) return; const isAbility = pokemon.ability === 'trace'; const possibleTargets = pokemon.adjacentFoes().filter( @@ -27,7 +27,7 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa neutralizinggas: { inherit: true, // Ability suppression implemented in sim/pokemon.ts:Pokemon#ignoringAbility - onPreStart(pokemon) { + onSwitchIn(pokemon) { this.add('-ability', pokemon, 'Neutralizing Gas'); pokemon.abilityState.ending = false; // Remove setter's innates before the ability starts diff --git a/data/mods/sharedpower/abilities.ts b/data/mods/sharedpower/abilities.ts index 8d815709d741..4cecc94b6a7a 100644 --- a/data/mods/sharedpower/abilities.ts +++ b/data/mods/sharedpower/abilities.ts @@ -2,7 +2,7 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa neutralizinggas: { inherit: true, // Ability suppression implemented in sim/pokemon.ts:Pokemon#ignoringAbility - onPreStart(pokemon) { + onSwitchIn(pokemon) { this.add('-ability', pokemon, 'Neutralizing Gas'); pokemon.abilityState.ending = false; // Remove setter's innates before the ability starts @@ -53,7 +53,7 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa trace: { inherit: true, onUpdate(pokemon) { - if (!pokemon.isStarted || this.effectState.gaveUp) return; + if (!this.effectState.seek) return; const isAbility = pokemon.ability === 'trace'; const possibleTargets = pokemon.adjacentFoes().filter( diff --git a/data/mods/sharingiscaring/items.ts b/data/mods/sharingiscaring/items.ts index ebf65b471007..bf0a4098e875 100644 --- a/data/mods/sharingiscaring/items.ts +++ b/data/mods/sharingiscaring/items.ts @@ -6,7 +6,7 @@ export const Items: import('../../../sim/dex-items').ModdedItemDataTable = { this.add('-enditem', target, 'Air Balloon'); if (target.item === 'airballoon') { target.item = ''; - target.itemState = {id: '', target}; + target.itemState.clear(); } else { delete target.volatiles['item:airballoon']; target.m.sharedItemsUsed.push('airballoon'); @@ -19,7 +19,7 @@ export const Items: import('../../../sim/dex-items').ModdedItemDataTable = { this.add('-enditem', target, 'Air Balloon'); if (target.item === 'airballoon') { target.item = ''; - target.itemState = {id: '', target}; + target.itemState.clear(); } else { delete target.volatiles['item:airballoon']; target.m.sharedItemsUsed.push('airballoon'); diff --git a/data/mods/sharingiscaring/scripts.ts b/data/mods/sharingiscaring/scripts.ts index 5222a9bc84c2..206abcbb7bb8 100644 --- a/data/mods/sharingiscaring/scripts.ts +++ b/data/mods/sharingiscaring/scripts.ts @@ -1,4 +1,4 @@ -import {RESTORATIVE_BERRIES} from '../../../sim/pokemon'; +import {EffectState, RESTORATIVE_BERRIES} from '../../../sim/pokemon'; export const Scripts: ModdedBattleScriptsData = { gen: 9, @@ -60,7 +60,7 @@ export const Scripts: ModdedBattleScriptsData = { } else { this.lastItem = this.item; this.item = ''; - this.itemState = {id: '', target: this}; + this.itemState.clear(); } this.usedItemThisTurn = true; this.battle.runEvent('AfterUseItem', this, null, null, item); @@ -104,7 +104,7 @@ export const Scripts: ModdedBattleScriptsData = { } else { this.lastItem = this.item; this.item = ''; - this.itemState = {id: '', target: this}; + this.itemState.clear(); } this.usedItemThisTurn = true; this.ateBerry = true; @@ -129,7 +129,7 @@ export const Scripts: ModdedBattleScriptsData = { const oldItem = this.getItem(); const oldItemState = this.itemState; this.item = item.id; - this.itemState = {id: item.id, target: this}; + this.itemState = new EffectState({id: item.id, target: this}, this.battle); if (oldItem.exists) this.battle.singleEvent('End', oldItem, oldItemState, this); if (item.id) { this.battle.singleEvent('Start', item, this.itemState, this, source, effect); diff --git a/data/moves.ts b/data/moves.ts index f850aa8b5ecc..349d3369d81d 100644 --- a/data/moves.ts +++ b/data/moves.ts @@ -1,5 +1,7 @@ // List of flags and their descriptions can be found in sim/dex-moves.ts +import {EffectState} from '../sim/pokemon'; + export const Moves: import('../sim/dex-moves').MoveDataTable = { "10000000voltthunderbolt": { num: 719, @@ -3768,7 +3770,7 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { this.queue.willMove(pokemon) || (pokemon === this.activePokemon && this.activeMove && !this.activeMove.isExternal) ) { - this.effectState.duration--; + this.effectState.duration!--; } if (!pokemon.lastMove) { this.debug(`Pokemon hasn't moved yet`); @@ -4909,7 +4911,7 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { this.effectState.move = move.id; this.add('-start', target, 'Encore'); if (!this.queue.willMove(target)) { - this.effectState.duration++; + this.effectState.duration!++; } }, onOverrideAction(pokemon, target, move) { @@ -7483,7 +7485,7 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { onSideStart(side) { this.add('-sidestart', side, 'move: G-Max Steelsurge'); }, - onEntryHazard(pokemon) { + onSwitchIn(pokemon) { if (pokemon.hasItem('heavydutyboots')) return; // Ice Face and Disguise correctly get typed damage from Stealth Rock // because Stealth Rock bypasses Substitute. @@ -8637,6 +8639,9 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { selfdestruct: "ifHit", slotCondition: 'healingwish', condition: { + onSwitchIn(target) { + this.singleEvent('Swap', this.effect, this.effectState, target); + }, onSwap(target) { if (!target.fainted && (target.hp < target.maxhp || target.status)) { target.heal(target.maxhp); @@ -10945,6 +10950,9 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { selfdestruct: "ifHit", slotCondition: 'lunardance', condition: { + onSwitchIn(target) { + this.singleEvent('Swap', this.effect, this.effectState, target); + }, onSwap(target) { if ( !target.fainted && ( @@ -17215,8 +17223,8 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { this.singleEvent('End', targetAbility, target.abilityState, target); source.ability = targetAbility.id; target.ability = sourceAbility.id; - source.abilityState = {id: this.toID(source.ability), target: source}; - target.abilityState = {id: this.toID(target.ability), target: target}; + source.abilityState = new EffectState({id: this.toID(source.ability), target: source}, this); + target.abilityState = new EffectState({id: this.toID(target.ability), target: target}, this); source.volatileStaleness = undefined; if (!target.isAlly(source)) target.volatileStaleness = 'external'; this.singleEvent('Start', targetAbility, source.abilityState, source); @@ -18163,7 +18171,7 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { this.add('-sidestart', side, 'Spikes'); this.effectState.layers++; }, - onEntryHazard(pokemon) { + onSwitchIn(pokemon) { if (!pokemon.isGrounded() || pokemon.hasItem('heavydutyboots')) return; const damageAmounts = [0, 3, 4, 6]; // 1/8, 1/6, 1/4 this.damage(damageAmounts[this.effectState.layers] * pokemon.maxhp / 24); @@ -18484,7 +18492,7 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { onSideStart(side) { this.add('-sidestart', side, 'move: Stealth Rock'); }, - onEntryHazard(pokemon) { + onSwitchIn(pokemon) { if (pokemon.hasItem('heavydutyboots')) return; const typeMod = this.clampIntRange(pokemon.runEffectiveness(this.dex.getActiveMove('stealthrock')), -6, 6); this.damage(pokemon.maxhp * Math.pow(2, typeMod) / 8); @@ -18612,7 +18620,7 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { onSideStart(side) { this.add('-sidestart', side, 'move: Sticky Web'); }, - onEntryHazard(pokemon) { + onSwitchIn(pokemon) { if (!pokemon.isGrounded() || pokemon.hasItem('heavydutyboots')) return; this.add('-activate', pokemon, 'move: Sticky Web'); this.boost({spe: -1}, pokemon, pokemon.side.foe.active[0], this.dex.getActiveMove('stickyweb')); @@ -19698,7 +19706,7 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { duration: 3, onStart(target) { if (target.activeTurns && !this.queue.willMove(target)) { - this.effectState.duration++; + this.effectState.duration!++; } this.add('-start', target, 'move: Taunt'); }, @@ -20509,7 +20517,7 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { this.add('-sidestart', side, 'move: Toxic Spikes'); this.effectState.layers++; }, - onEntryHazard(pokemon) { + onSwitchIn(pokemon) { if (!pokemon.isGrounded()) return; if (pokemon.hasType('Poison')) { this.add('-sideend', pokemon.side, 'move: Toxic Spikes', '[of] ' + pokemon); @@ -21725,9 +21733,9 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { } }, onResidualOrder: 4, - onResidual(side: any) { + onResidual(target: Pokemon) { if (this.getOverflowedTurnCount() <= this.effectState.startingTurn) return; - side.removeSlotCondition(this.getAtSlot(this.effectState.sourceSlot), 'wish'); + target.side.removeSlotCondition(this.getAtSlot(this.effectState.sourceSlot), 'wish'); }, onEnd(target) { if (target && !target.fainted) { diff --git a/data/random-battles/gen7/teams.ts b/data/random-battles/gen7/teams.ts index 413e3767ca67..1b76f588b5ef 100644 --- a/data/random-battles/gen7/teams.ts +++ b/data/random-battles/gen7/teams.ts @@ -1361,9 +1361,9 @@ export class RandomGen7Teams extends RandomGen8Teams { if (item.megaStone || species.name === 'Rayquaza-Mega') hasMega = true; if (item.zMove) teamDetails.zMove = 1; if (set.ability === 'Snow Warning' || set.moves.includes('hail')) teamDetails.hail = 1; - if (set.moves.includes('raindance') || set.ability === 'Drizzle' && !item.onPrimal) teamDetails.rain = 1; + if (set.moves.includes('raindance') || set.ability === 'Drizzle' && !item.isPrimalOrb) teamDetails.rain = 1; if (set.ability === 'Sand Stream') teamDetails.sand = 1; - if (set.moves.includes('sunnyday') || set.ability === 'Drought' && !item.onPrimal) teamDetails.sun = 1; + if (set.moves.includes('sunnyday') || set.ability === 'Drought' && !item.isPrimalOrb) teamDetails.sun = 1; if (set.moves.includes('aromatherapy') || set.moves.includes('healbell')) teamDetails.statusCure = 1; if (set.moves.includes('spikes')) teamDetails.spikes = (teamDetails.spikes || 0) + 1; if (set.moves.includes('stealthrock')) teamDetails.stealthRock = 1; diff --git a/server/chat-plugins/othermetas.ts b/server/chat-plugins/othermetas.ts index 579311408f24..e34caaf6613b 100644 --- a/server/chat-plugins/othermetas.ts +++ b/server/chat-plugins/othermetas.ts @@ -41,7 +41,7 @@ function getMegaStone(stone: string, mod = 'gen9'): Item | null { return null; } } - if (!item.megaStone && !item.onPrimal && !item.forcedForme?.endsWith('Epilogue') && + if (!item.megaStone && !item.isPrimalOrb && !item.forcedForme?.endsWith('Epilogue') && !item.forcedForme?.endsWith('Origin') && !item.name.startsWith('Rusted') && !item.name.endsWith('Mask')) return null; return item; } diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts index 9a6b865c71f3..ef1752b82b7f 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -135,22 +135,21 @@ export class BattleActions { for (const moveSlot of pokemon.moveSlots) { moveSlot.used = false; } + pokemon.abilityState.effectOrder = this.battle.effectOrder++; + pokemon.itemState.effectOrder = this.battle.effectOrder++; this.battle.runEvent('BeforeSwitchIn', pokemon); if (sourceEffect) { this.battle.add(isDrag ? 'drag' : 'switch', pokemon, pokemon.getDetails, '[from] ' + sourceEffect); } else { this.battle.add(isDrag ? 'drag' : 'switch', pokemon, pokemon.getDetails); } - pokemon.abilityOrder = this.battle.abilityOrder++; if (isDrag && this.battle.gen === 2) pokemon.draggedIn = this.battle.turn; pokemon.previouslySwitchedIn++; if (isDrag && this.battle.gen >= 5) { // runSwitch happens immediately so that Mold Breaker can make hazards bypass Clear Body and Levitate - this.battle.singleEvent('PreStart', pokemon.getAbility(), pokemon.abilityState, pokemon); this.runSwitch(pokemon); } else { - this.battle.queue.insertChoice({choice: 'runUnnerve', pokemon}); this.battle.queue.insertChoice({choice: 'runSwitch', pokemon}); } @@ -170,17 +169,22 @@ export class BattleActions { return true; } runSwitch(pokemon: Pokemon) { - this.battle.runEvent('Swap', pokemon); - if (this.battle.gen >= 5) { - this.battle.runEvent('SwitchIn', pokemon); + const switchersIn = [pokemon]; + for (let a = this.battle.queue.peek(); a?.choice === 'runSwitch'; a = this.battle.queue.peek()) { + const nextSwitch = this.battle.queue.shift(); + switchersIn.push(nextSwitch!.pokemon!); + } + this.battle.fieldEvent('SwitchIn', switchersIn); + + if (!pokemon.hp) return false; + pokemon.isStarted = true; + pokemon.draggedIn = null; + return true; } - this.battle.runEvent('EntryHazard', pokemon); - if (this.battle.gen <= 4) { - this.battle.runEvent('SwitchIn', pokemon); - } + this.battle.runEvent('SwitchIn', pokemon); if (this.battle.gen <= 2) { // pokemon.lastMove is reset for all Pokemon on the field after a switch. This affects Mirror Move. @@ -329,6 +333,7 @@ export class BattleActions { this.battle.add('-hint', `Some effects can force a Pokemon to use ${move.name} again in a row.`); } + // TODO: Refactor to use BattleQueue#prioritizeAction in onAnyAfterMove handlers // Dancer's activation order is completely different from any other event, so it's handled separately if (move.flags['dance'] && moveDidSomething && !move.isExternal) { const dancers = []; @@ -343,7 +348,7 @@ export class BattleActions { // but before any multipliers like Agility or Choice Scarf // Ties go to whichever Pokemon has had the ability for the least amount of time dancers.sort( - (a, b) => -(b.storedStats['spe'] - a.storedStats['spe']) || b.abilityOrder - a.abilityOrder + (a, b) => -(b.storedStats['spe'] - a.storedStats['spe']) || b.abilityState.effectOrder - a.abilityState.effectOrder ); const targetOf1stDance = this.battle.activeTarget!; for (const dancer of dancers) { diff --git a/sim/battle.ts b/sim/battle.ts index 5f2ea0586401..659a9f5b3640 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -24,6 +24,7 @@ import {State} from './state'; import {BattleQueue, Action} from './battle-queue'; import {BattleActions} from './battle-actions'; import {Utils} from '../lib/utils'; +import {ItemData} from './dex-items'; declare const __version: any; export type ChannelID = 0 | 1 | 2 | 3 | 4; @@ -171,7 +172,7 @@ export class Battle { lastMoveLine: number; /** The last damage dealt by a move in the battle - only used by Gen 1 Counter. */ lastDamage: number; - abilityOrder: number; + effectOrder: number; quickClawRoll: boolean; teamGenerator: ReturnType | null; @@ -214,7 +215,7 @@ export class Battle { options.forceRandomChance : null; this.deserialized = !!options.deserialized; this.strictChoices = !!options.strictChoices; - this.formatData = {id: format.id}; + this.formatData = new EffectState({id: format.id}, this); this.gameType = (format.gameType || 'singles'); this.field = new Field(this); this.sides = Array(format.playerCount).fill(null) as any; @@ -244,7 +245,7 @@ export class Battle { this.ended = false; this.effect = {id: ''} as Effect; - this.effectState = {id: ''}; + this.effectState = new EffectState({id: ''}, this); this.event = {id: ''}; this.events = null; @@ -258,7 +259,7 @@ export class Battle { this.lastMoveLine = -1; this.lastSuccessfulMoveThisTurn = null; this.lastDamage = 0; - this.abilityOrder = 0; + this.effectOrder = 0; this.quickClawRoll = false; this.teamGenerator = null; @@ -401,13 +402,15 @@ export class Battle { ((b.priority || 0) - (a.priority || 0)) || ((b.speed || 0) - (a.speed || 0)) || -((b.subOrder || 0) - (a.subOrder || 0)) || + -((b.state?.effectOrder || 0) - (a.state?.effectOrder || 0)) || 0; } static compareRedirectOrder(a: AnyObject, b: AnyObject) { return ((b.priority || 0) - (a.priority || 0)) || ((b.speed || 0) - (a.speed || 0)) || - ((a.effectHolder && b.effectHolder) ? -(b.effectHolder.abilityOrder - a.effectHolder.abilityOrder) : 0) || + ((a.effectHolder?.abilityState && b.effectHolder?.abilityState) ? + -(b.effectHolder.abilityState.effectOrder - a.effectHolder.abilityState.effectOrder) : 0) || 0; } @@ -471,21 +474,30 @@ export class Battle { /** * Runs an event with no source on each effect on the field, in Speed order. * - * Unlike `eachEvent`, this contains a lot of other handling and is intended only for the residual step. + * Unlike `eachEvent`, this contains a lot of other handling and is only intended for + * the 'Residual' and 'SwitchIn' events. */ - residualEvent(eventid: string, relayVar?: any) { + fieldEvent(eventid: string, targets?: Pokemon[]) { const callbackName = `on${eventid}`; - let handlers = this.findBattleEventHandlers(callbackName, 'duration'); - handlers = handlers.concat(this.findFieldEventHandlers(this.field, `onField${eventid}`, 'duration')); + let getKey: undefined | 'duration'; + if (eventid === 'Residual') { + getKey = 'duration'; + } + let handlers = this.findFieldEventHandlers(this.field, `onField${eventid}`, getKey); for (const side of this.sides) { if (side.n < 2 || !side.allySide) { - handlers = handlers.concat(this.findSideEventHandlers(side, `onSide${eventid}`, 'duration')); + handlers = handlers.concat(this.findSideEventHandlers(side, `onSide${eventid}`, getKey)); } for (const active of side.active) { if (!active) continue; - handlers = handlers.concat(this.findPokemonEventHandlers(active, callbackName, 'duration')); + if (eventid === 'SwitchIn') { + handlers = handlers.concat(this.findPokemonEventHandlers(active, `onAny${eventid}`)); + } + if (targets && !targets.includes(active)) continue; + handlers = handlers.concat(this.findPokemonEventHandlers(active, callbackName, getKey)); handlers = handlers.concat(this.findSideEventHandlers(side, callbackName, undefined, active)); handlers = handlers.concat(this.findFieldEventHandlers(this.field, callbackName, undefined, active)); + handlers = handlers.concat(this.findBattleEventHandlers(callbackName, getKey, active)); } } this.speedSort(handlers); @@ -494,7 +506,7 @@ export class Battle { handlers.shift(); const effect = handler.effect; if ((handler.effectHolder as Pokemon).fainted) continue; - if (handler.end && handler.state && handler.state.duration) { + if (eventid === 'Residual' && handler.end && handler.state && handler.state.duration) { handler.state.duration--; if (!handler.state.duration) { const endCallArgs = handler.endCallArgs || [handler.effectHolder, effect.id]; @@ -508,7 +520,7 @@ export class Battle { if ((handler.effectHolder as Side).sideConditions) handlerEventid = `Side${eventid}`; if ((handler.effectHolder as Field).pseudoWeather) handlerEventid = `Field${eventid}`; if (handler.callback) { - this.singleEvent(handlerEventid, effect, handler.state, handler.effectHolder, null, null, relayVar, handler.callback); + this.singleEvent(handlerEventid, effect, handler.state, handler.effectHolder, null, null, undefined, handler.callback); } this.faintMessages(); @@ -518,7 +530,7 @@ export class Battle { /** The entire event system revolves around this function and runEvent. */ singleEvent( - eventid: string, effect: Effect, state: AnyObject | null, + eventid: string, effect: Effect, state: EffectState | Record | null, target: string | Pokemon | Side | Field | Battle | null, source?: string | Pokemon | Effect | false | null, sourceEffect?: Effect | string | null, relayVar?: any, customCallback?: unknown ) { @@ -548,8 +560,9 @@ export class Battle { // it's changed; call it off return relayVar; } - if (eventid !== 'Start' && eventid !== 'TakeItem' && eventid !== 'Primal' && - effect.effectType === 'Item' && (target instanceof Pokemon) && target.ignoringItem()) { + if (eventid !== 'Start' && eventid !== 'TakeItem' && effect.effectType === 'Item' && + !(eventid === 'SwitchIn' && (effect as ItemData).onSwitchInPriority === -1) && // <- questionable hack + (target instanceof Pokemon) && target.ignoringItem()) { this.debug(eventid + ' handler suppressed by Embargo, Klutz or Magic Room'); return relayVar; } @@ -573,7 +586,7 @@ export class Battle { const parentEvent = this.event; this.effect = effect; - this.effectState = state || {}; + this.effectState = state as EffectState || new EffectState({}, this); this.event = {id: eventid, target, source, effect: sourceEffect}; this.eventDepth++; @@ -726,7 +739,7 @@ export class Battle { if (callback !== undefined) { if (Array.isArray(target)) throw new Error(""); handlers.unshift(this.resolvePriority({ - effect: sourceEffect, callback, state: {}, end: null, effectHolder: target, + effect: sourceEffect, callback, state: new EffectState({}, this), end: null, effectHolder: target, }, `on${eventid}`)); } } @@ -839,7 +852,7 @@ export class Battle { const parentEffect = this.effect; const parentEffectState = this.effectState; this.effect = handler.effect; - this.effectState = handler.state || {}; + this.effectState = handler.state || new EffectState({}, this); this.effectState.target = effectHolder; returnVal = handler.callback.apply(this, args); @@ -888,17 +901,57 @@ export class Battle { return this.runEvent(eventid, target, source, effect, relayVar, onEffect, true); } - resolvePriority(handler: EventListenerWithoutPriority, callbackName: string) { + resolvePriority(h: EventListenerWithoutPriority, callbackName: string) { + const handler = h as EventListener; // @ts-ignore handler.order = handler.effect[`${callbackName}Order`] || false; // @ts-ignore handler.priority = handler.effect[`${callbackName}Priority`] || 0; // @ts-ignore handler.subOrder = handler.effect[`${callbackName}SubOrder`] || 0; + if (!handler.subOrder) { + // https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-59#post-8685465 + const effectTypeOrder: {[k in EffectType]?: number} = { + // Z-Move: 1, + Condition: 2, + // Slot Condition: 3, + // Side Condition: 4, + // Field Condition: 5, (includes weather but also terrains and pseudoweathers) + Weather: 5, + Format: 5, + Rule: 5, + Ruleset: 5, + // Poison Touch: 6, + Ability: 7, + Item: 8, + // Stall: 9, + }; + handler.subOrder = effectTypeOrder[handler.effect.effectType] || 0; + if (handler.effect.effectType === 'Condition') { + if (handler.state?.target instanceof Side) { + if (handler.state.isSlotCondition) { + // slot condition + handler.subOrder = 3; + } else { + // side condition + handler.subOrder = 4; + } + } else if (handler.state?.target instanceof Field) { + // field condition + handler.subOrder = 5; + } + } else if (handler.effect.effectType === 'Ability') { + if (handler.effect.name === 'Poison Touch') { + handler.subOrder = 6; + } else if (handler.effect.name === 'Stall') { + handler.subOrder = 9; + } + } + } if (handler.effectHolder && (handler.effectHolder as Pokemon).getStat) { - (handler as EventListener).speed = (handler.effectHolder as Pokemon).speed; + handler.speed = (handler.effectHolder as Pokemon).speed; } - return handler as EventListener; + return handler; } findEventHandlers(target: Pokemon | Pokemon[] | Side | Battle, eventName: string, source?: Pokemon | null) { @@ -1009,7 +1062,7 @@ export class Battle { state: slotConditionState, end: side.removeSlotCondition, endCallArgs: [side, pokemon, slotCondition.id], - effectHolder: side, + effectHolder: pokemon, }, callbackName)); } } @@ -1017,7 +1070,7 @@ export class Battle { return handlers; } - findBattleEventHandlers(callbackName: string, getKey?: 'duration') { + findBattleEventHandlers(callbackName: string, getKey?: 'duration', customHolder?: Pokemon) { const handlers: EventListener[] = []; let callback; @@ -1026,15 +1079,15 @@ export class Battle { callback = format[callbackName]; if (callback !== undefined || (getKey && this.formatData[getKey])) { handlers.push(this.resolvePriority({ - effect: format, callback, state: this.formatData, end: null, effectHolder: this, + effect: format, callback, state: this.formatData, end: null, effectHolder: customHolder || this, }, callbackName)); } if (this.events && (callback = this.events[callbackName]) !== undefined) { for (const handler of callback) { const state = (handler.target.effectType === 'Format') ? this.formatData : null; handlers.push({ - effect: handler.target, callback: handler.callback, state, end: null, - effectHolder: this, priority: handler.priority, order: handler.order, subOrder: handler.subOrder, + effect: handler.target, callback: handler.callback, state, end: null, effectHolder: customHolder || this, + priority: handler.priority, order: handler.order, subOrder: handler.subOrder, }); } } @@ -2642,9 +2695,6 @@ export class Battle { this.add('-heal', action.target, action.target.getHealth, '[from] move: Revival Blessing'); action.pokemon.side.removeSlotCondition(action.pokemon, 'revivalblessing'); break; - case 'runUnnerve': - this.singleEvent('PreStart', action.pokemon.getAbility(), action.pokemon.abilityState, action.pokemon); - break; case 'runSwitch': this.actions.runSwitch(action.pokemon); break; @@ -2667,7 +2717,7 @@ export class Battle { this.clearActiveMove(true); this.updateSpeed(); residualPokemon = this.getAllActive().map(pokemon => [pokemon, pokemon.getUndynamaxedHP()] as const); - this.residualEvent('Residual'); + this.fieldEvent('Residual'); this.add('upkeep'); break; } @@ -2711,7 +2761,7 @@ export class Battle { return false; } - if (this.gen >= 5) { + if (this.gen >= 5 && action.choice !== 'start') { this.eachEvent('Update'); for (const [pokemon, originalHP] of residualPokemon) { const maxhp = pokemon.getUndynamaxedHP(pokemon.maxhp); diff --git a/sim/dex-abilities.ts b/sim/dex-abilities.ts index 5d60ee825d29..6e8c2d6d72ea 100644 --- a/sim/dex-abilities.ts +++ b/sim/dex-abilities.ts @@ -5,7 +5,6 @@ import {Utils} from '../lib/utils'; interface AbilityEventMethods { onCheckShow?: (this: Battle, pokemon: Pokemon) => void; onEnd?: (this: Battle, target: Pokemon & Side & Field) => void; - onPreStart?: (this: Battle, pokemon: Pokemon) => void; onStart?: (this: Battle, target: Pokemon) => void; } @@ -94,6 +93,14 @@ export class DexAbilities { ability = this.get(this.dex.data.Aliases[id]); } else if (id && this.dex.data.Abilities.hasOwnProperty(id)) { const abilityData = this.dex.data.Abilities[id] as any; + if (this.dex.gen >= 5) { + // Abilities and items Start at different times during the SwitchIn event, so we do this + // instead of running the Start event during switch-ins + // gens 4 and before still use the old system, though + if (abilityData.onStart && !abilityData.onSwitchIn) { + abilityData.onSwitchIn = abilityData.onStart; + } + } const abilityTextData = this.dex.getDescs('Abilities', id, abilityData); ability = new Ability({ name: id, diff --git a/sim/dex-conditions.ts b/sim/dex-conditions.ts index 11e982b0af78..08b2d1ec0919 100644 --- a/sim/dex-conditions.ts +++ b/sim/dex-conditions.ts @@ -2,6 +2,14 @@ import {Utils} from '../lib/utils'; import {assignMissingFields, BasicEffect, toID} from './dex-data'; import type {SecondaryEffect, MoveEventMethods} from './dex-moves'; +/** + * Event method prefixes: + * Ally: triggers for each ally (including the effect holder itself) that is a target of the event, i.e. Pastel Veil + * Foe: triggers for each foe that is a target of the event, i.e. Unnerve + * Source: triggers for the source of the event; events must have a source parameter to trigger these handlers + * Any: triggers for each target of the event regardless of the holder's relation to it + */ + export interface EventMethods { onDamagingHit?: (this: Battle, damage: number, target: Pokemon, source: Pokemon, move: ActiveMove) => void; onEmergencyExit?: (this: Battle, pokemon: Pokemon) => void; @@ -184,7 +192,6 @@ export interface EventMethods { ) => boolean | null | void; onFoeSetWeather?: (this: Battle, target: Pokemon, source: Pokemon, weather: Condition) => boolean | void; onFoeStallMove?: (this: Battle, pokemon: Pokemon) => boolean | void; - onFoeSwitchIn?: (this: Battle, pokemon: Pokemon) => void; onFoeSwitchOut?: (this: Battle, pokemon: Pokemon) => void; onFoeTakeItem?: ( (this: Battle, item: Item, pokemon: Pokemon, source: Pokemon, move?: ActiveMove) => boolean | void @@ -285,7 +292,6 @@ export interface EventMethods { ) => boolean | null | void; onSourceSetWeather?: (this: Battle, target: Pokemon, source: Pokemon, weather: Condition) => boolean | void; onSourceStallMove?: (this: Battle, pokemon: Pokemon) => boolean | void; - onSourceSwitchIn?: (this: Battle, pokemon: Pokemon) => void; onSourceSwitchOut?: (this: Battle, pokemon: Pokemon) => void; onSourceTakeItem?: ( (this: Battle, item: Item, pokemon: Pokemon, source: Pokemon, move?: ActiveMove) => boolean | void @@ -426,6 +432,8 @@ export interface EventMethods { onAnyModifyAccuracyPriority?: number; onAnyFaintPriority?: number; onAnyPrepareHitPriority?: number; + onAnySwitchInPriority?: number; + onAnySwitchInSubOrder?: number; onAllyBasePowerPriority?: number; onAllyModifyAtkPriority?: number; onAllyModifySpAPriority?: number; @@ -470,6 +478,7 @@ export interface EventMethods { onSourceModifyDamagePriority?: number; onSourceModifySpAPriority?: number; onSwitchInPriority?: number; + onSwitchInSubOrder?: number; onTrapPokemonPriority?: number; onTryBoostPriority?: number; onTryEatItemPriority?: number; @@ -616,6 +625,7 @@ export class Condition extends BasicEffect implements Readonly { declare readonly effectType: 'Condition' | 'Weather' | 'Status' | 'Terastal'; declare readonly counterMax?: number; + declare effectOrder?: number; declare readonly durationCallback?: (this: Battle, target: Pokemon, source: Pokemon, effect: Effect | null) => number; declare readonly onCopy?: (this: Battle, pokemon: Pokemon) => void; diff --git a/sim/dex-items.ts b/sim/dex-items.ts index b0db83b3aa02..28ba571054ee 100644 --- a/sim/dex-items.ts +++ b/sim/dex-items.ts @@ -92,6 +92,8 @@ export class Item extends BasicEffect implements Readonly { readonly isGem: boolean; /** Is this item a Pokeball? */ readonly isPokeball: boolean; + /** Is this item a Red or Blue Orb? */ + readonly isPrimalOrb: boolean; declare readonly condition?: ConditionData; declare readonly forcedForme?: string; @@ -101,7 +103,7 @@ export class Item extends BasicEffect implements Readonly { declare readonly boosts?: SparseBoostsTable | false; declare readonly onEat?: ((this: Battle, pokemon: Pokemon) => void) | false; - declare readonly onPrimal?: (this: Battle, pokemon: Pokemon) => void; + declare readonly onUse?: ((this: Battle, pokemon: Pokemon) => void) | false; declare readonly onStart?: (this: Battle, target: Pokemon) => void; declare readonly onEnd?: (this: Battle, target: Pokemon) => void; @@ -124,6 +126,7 @@ export class Item extends BasicEffect implements Readonly { this.onPlate = data.onPlate || undefined; this.isGem = !!data.isGem; this.isPokeball = !!data.isPokeball; + this.isPrimalOrb = !!data.isPrimalOrb; if (!this.gen) { if (this.num >= 1124) { @@ -190,6 +193,14 @@ export class DexItems { } if (id && this.dex.data.Items.hasOwnProperty(id)) { const itemData = this.dex.data.Items[id] as any; + if (this.dex.gen >= 5) { + // Abilities and items Start at different times during the SwitchIn event, so we do this + // instead of running the Start event during switch-ins + // gens 4 and before still use the old system, though + if (itemData.onStart && !itemData.onSwitchIn) { + itemData.onSwitchIn = itemData.onStart; + } + } const itemTextData = this.dex.getDescs('Items', id, itemData); item = new Item({ name: id, diff --git a/sim/field.ts b/sim/field.ts index 067c92d1b02e..7a972ed2b8c9 100644 --- a/sim/field.ts +++ b/sim/field.ts @@ -26,9 +26,9 @@ export class Field { this.id = ''; this.weather = ''; - this.weatherState = {id: ''}; + this.weatherState = new EffectState({id: ''}, battle); this.terrain = ''; - this.terrainState = {id: ''}; + this.terrainState = new EffectState({id: ''}, battle); this.pseudoWeather = {}; } @@ -67,7 +67,7 @@ export class Field { const prevWeather = this.weather; const prevWeatherState = this.weatherState; this.weather = status.id; - this.weatherState = {id: status.id}; + this.weatherState = new EffectState({id: status.id}, this.battle); if (source) { this.weatherState.source = source; this.weatherState.sourceSlot = source.getSlot(); @@ -93,7 +93,7 @@ export class Field { const prevWeather = this.getWeather(); this.battle.singleEvent('FieldEnd', prevWeather, this.weatherState, this); this.weather = ''; - this.weatherState = {id: ''}; + this.weatherState.clear(); this.battle.eachEvent('WeatherChange'); return true; } @@ -138,12 +138,12 @@ export class Field { const prevTerrain = this.terrain; const prevTerrainState = this.terrainState; this.terrain = status.id; - this.terrainState = { + this.terrainState = new EffectState({ id: status.id, source, sourceSlot: source.getSlot(), duration: status.duration, - }; + }, this.battle); if (status.durationCallback) { this.terrainState.duration = status.durationCallback.call(this.battle, source, source, sourceEffect); } @@ -161,7 +161,7 @@ export class Field { const prevTerrain = this.getTerrain(); this.battle.singleEvent('FieldEnd', prevTerrain, this.terrainState, this); this.terrain = ''; - this.terrainState = {id: ''}; + this.terrainState.clear(); this.battle.eachEvent('TerrainChange'); return true; } @@ -197,12 +197,12 @@ export class Field { if (!(status as any).onFieldRestart) return false; return this.battle.singleEvent('FieldRestart', status, state, this, source, sourceEffect); } - state = this.pseudoWeather[status.id] = { + state = this.pseudoWeather[status.id] = new EffectState({ id: status.id, source, sourceSlot: source?.getSlot(), duration: status.duration, - }; + }, this.battle); if (status.durationCallback) { if (!source) throw new Error(`setting fieldcond without a source`); state.duration = status.durationCallback.call(this.battle, source, source, sourceEffect); diff --git a/sim/pokemon.ts b/sim/pokemon.ts index a53d4e7d011c..6f250296bd1e 100644 --- a/sim/pokemon.ts +++ b/sim/pokemon.ts @@ -30,10 +30,34 @@ interface Attacker { damageValue?: (number | boolean | undefined); } -export interface EffectState { - // TODO: set this to be an actual number after converting data/ to .ts - duration?: number | any; +export class EffectState { + id: string; + effectOrder: number; + duration?: number; [k: string]: any; + + constructor(data: AnyObject, battle: Battle) { + this.id = data.id || ''; + Object.assign(this, data); + if (this.id && this.target && (!(this.target instanceof Pokemon) || this.target.isActive)) { + this.effectOrder = battle.effectOrder++; + } else { + this.effectOrder = 0; + } + } + + clear() { + this.id = ''; + for (const k in this) { + if (k === 'id' || k === 'target') { + continue; + } else if (k === 'effectOrder') { + this.effectOrder = 0; + } else { + delete this[k]; + } + } + } } // Berries which restore PP/HP and thus inflict external staleness when given to an opponent as @@ -250,7 +274,6 @@ export class Pokemon { weighthg: number; speed: number; - abilityOrder: number; canMegaEvo: string | null | undefined; canMegaEvoX: string | null | undefined; @@ -309,7 +332,7 @@ export class Pokemon { if (set.name === set.species || !set.name) { set.name = this.baseSpecies.baseSpecies; } - this.speciesState = {id: this.species.id}; + this.speciesState = new EffectState({id: this.species.id}, this.battle); this.name = set.name.substr(0, 20); this.fullname = this.side.id + ': ' + this.name; @@ -357,7 +380,7 @@ export class Pokemon { (this.gender === '' ? '' : ', ' + this.gender) + (this.set.shiny ? ', shiny' : ''); this.status = ''; - this.statusState = {}; + this.statusState = new EffectState({}, this.battle); this.volatiles = {}; this.showCure = undefined; @@ -400,10 +423,10 @@ export class Pokemon { this.baseAbility = toID(set.ability); this.ability = this.baseAbility; - this.abilityState = {id: this.ability}; + this.abilityState = new EffectState({id: this.ability}, this.battle); this.item = toID(set.item); - this.itemState = {id: this.item}; + this.itemState = new EffectState({id: this.item}, this.battle); this.lastItem = ''; this.usedItemThisTurn = false; this.ateBerry = false; @@ -461,12 +484,6 @@ export class Pokemon { this.weighthg = 1; this.speed = 0; - /** - * Determines the order in which redirect abilities like Lightning Rod - * activate if speed tied. Surprisingly not random like every other speed - * tie, but based on who first switched in or acquired the ability! - */ - this.abilityOrder = 0; this.canMegaEvo = this.battle.actions.canMegaEvo(this); this.canMegaEvoX = this.battle.actions.canMegaEvoX?.(this); @@ -1201,7 +1218,7 @@ export class Pokemon { if (switchCause === 'shedtail' && i !== 'substitute') continue; if (this.battle.dex.conditions.getByID(i as ID).noCopy) continue; // shallow clones - this.volatiles[i] = {...pokemon.volatiles[i]}; + this.volatiles[i] = new EffectState(pokemon.volatiles[i], this.battle); if (this.volatiles[i].linkedPokemon) { delete pokemon.volatiles[i].linkedPokemon; delete pokemon.volatiles[i].linkedStatus; @@ -1400,7 +1417,7 @@ export class Pokemon { if (source.zMove) { this.battle.add('-burst', this, apparentSpecies, species.requiredItem); this.moveThisTurnResult = true; // Ultra Burst counts as an action for Truant - } else if (source.onPrimal) { + } else if (source.isPrimalOrb) { if (this.illusion) { this.ability = ''; this.battle.add('-primal', this.illusion, species.requiredItem); @@ -1667,7 +1684,7 @@ export class Pokemon { } this.status = status.id; - this.statusState = {id: status.id, target: this}; + this.statusState = new EffectState({id: status.id, target: this}, this.battle); if (source) this.statusState.source = source; if (status.duration) this.statusState.duration = status.duration; if (status.durationCallback) { @@ -1733,7 +1750,7 @@ export class Pokemon { this.lastItem = this.item; this.item = ''; - this.itemState = {id: '', target: this}; + this.itemState.clear(); this.usedItemThisTurn = true; this.ateBerry = true; this.battle.runEvent('AfterUseItem', this, null, null, item); @@ -1770,7 +1787,7 @@ export class Pokemon { this.lastItem = this.item; this.item = ''; - this.itemState = {id: '', target: this}; + this.itemState.clear(); this.usedItemThisTurn = true; this.battle.runEvent('AfterUseItem', this, null, null, item); return true; @@ -1790,7 +1807,7 @@ export class Pokemon { if (this.battle.runEvent('TakeItem', this, source, null, item)) { this.item = ''; const oldItemState = this.itemState; - this.itemState = {id: '', target: this}; + this.itemState.clear(); this.pendingStaleness = undefined; this.battle.singleEvent('End', item, oldItemState, this); this.battle.runEvent('AfterTakeItem', this, null, null, item); @@ -1815,7 +1832,7 @@ export class Pokemon { const oldItem = this.getItem(); const oldItemState = this.itemState; this.item = item.id; - this.itemState = {id: item.id, target: this}; + this.itemState = new EffectState({id: item.id, target: this}, this.battle); if (oldItem.exists) this.battle.singleEvent('End', oldItem, oldItemState, this); if (item.id) { this.battle.singleEvent('Start', item, this.itemState, this, source, effect); @@ -1857,12 +1874,11 @@ export class Pokemon { this.battle.dex.moves.get(this.battle.effect.id)); } this.ability = ability.id; - this.abilityState = {id: ability.id, target: this}; + this.abilityState = new EffectState({id: ability.id, target: this}, this.battle); if (ability.id && this.battle.gen > 3 && (!isTransform || oldAbility !== ability.id || this.battle.gen <= 4)) { this.battle.singleEvent('Start', ability, this.abilityState, this, source); } - this.abilityOrder = this.battle.abilityOrder++; return oldAbility; } @@ -1917,7 +1933,7 @@ export class Pokemon { this.battle.debug('add volatile [' + status.id + '] interrupted'); return result; } - this.volatiles[status.id] = {id: status.id, name: status.name, target: this}; + this.volatiles[status.id] = new EffectState({id: status.id, name: status.name, target: this}, this.battle); if (source) { this.volatiles[status.id].source = source; this.volatiles[status.id].sourceSlot = source.getSlot(); diff --git a/sim/side.ts b/sim/side.ts index a4707d65c967..491b00c5b71f 100644 --- a/sim/side.ts +++ b/sim/side.ts @@ -294,13 +294,13 @@ export class Side { if (!(status as any).onSideRestart) return false; return this.battle.singleEvent('SideRestart', status, this.sideConditions[status.id], this, source, sourceEffect); } - this.sideConditions[status.id] = { + this.sideConditions[status.id] = new EffectState({ id: status.id, target: this, source, sourceSlot: source.getSlot(), duration: status.duration, - }; + }, this.battle); if (status.durationCallback) { this.sideConditions[status.id].duration = status.durationCallback.call(this.battle, this.active[0], source, sourceEffect); @@ -346,13 +346,14 @@ export class Side { if (!status.onRestart) return false; return this.battle.singleEvent('Restart', status, this.slotConditions[target][status.id], this, source, sourceEffect); } - const conditionState = this.slotConditions[target][status.id] = { + const conditionState = this.slotConditions[target][status.id] = new EffectState({ id: status.id, target: this, source, sourceSlot: source.getSlot(), + isSlotCondition: true, duration: status.duration, - }; + }, this.battle); if (status.durationCallback) { conditionState.duration = status.durationCallback.call(this.battle, this.active[0], source, sourceEffect); diff --git a/sim/state.ts b/sim/state.ts index 7a96d6024385..0a4fcdef4935 100644 --- a/sim/state.ts +++ b/sim/state.ts @@ -12,7 +12,7 @@ import {Battle} from './battle'; import {Dex} from './dex'; import {Field} from './field'; -import {Pokemon} from './pokemon'; +import {EffectState, Pokemon} from './pokemon'; import {PRNG} from './prng'; import {Choice, Side} from './side'; @@ -368,8 +368,8 @@ export const State = new class { // NOTE: see explanation on the declaration above for why this must be defined lazily. if (!this.REFERABLE) { this.REFERABLE = new Set([ - Battle, Field, Side, Pokemon, Dex.Condition, - Dex.Ability, Dex.Item, Dex.Move, Dex.Species, + Battle, Field, Side, Pokemon, EffectState, + Dex.Condition, Dex.Ability, Dex.Item, Dex.Move, Dex.Species, ]); } return this.REFERABLE.has(obj.constructor); diff --git a/test/sim/abilities/commander.js b/test/sim/abilities/commander.js index 9cf3b72af474..dc135bd72f50 100644 --- a/test/sim/abilities/commander.js +++ b/test/sim/abilities/commander.js @@ -115,10 +115,10 @@ describe('Commander', function () { const dondozo = battle.p2.active[1]; assert.statStage(tatsugiri, 'atk', -1); + assert.equal(battle.requestState, 'move', 'It should not have switched out on Eject Pack'); assert.holdsItem(tatsugiri); assert.statStage(dondozo, 'atk', 1); assert.holdsItem(dondozo); - assert.equal(battle.requestState, 'move', 'It should not have switched out on Eject Pack'); battle.makeChoices('move tackle 2, move trick 2', 'auto'); assert.holdsItem(dondozo); @@ -241,7 +241,7 @@ describe('Commander', function () { assert.false.fullHP(shuckle, `Shuckle should have taken damage from Dazzling Gleam`); }); - it.skip(`should activate after hazards run`, function () { + it(`should activate after hazards run`, function () { battle = common.createBattle({gameType: 'doubles'}, [[ {species: 'regieleki', moves: ['toxicspikes']}, {species: 'registeel', moves: ['sleeptalk']}, diff --git a/test/sim/abilities/iceface.js b/test/sim/abilities/iceface.js index d0e4fd6bf7c9..240844730e13 100644 --- a/test/sim/abilities/iceface.js +++ b/test/sim/abilities/iceface.js @@ -59,7 +59,7 @@ describe('Ice Face', function () { assert.false(hasMultipleActivates, "Ice Face should not trigger when being KOed. Only one |-activate| should exist in this test."); }); - it.skip(`should reform Ice Face on switchin after all entrance Abilities occur`, function () { + it(`should reform Ice Face on switchin after all entrance Abilities occur`, function () { battle = common.createBattle([[ {species: 'Eiscue', ability: 'iceface', moves: ['sleeptalk']}, {species: 'Abomasnow', ability: 'snowwarning', moves: ['sleeptalk']}, diff --git a/test/sim/abilities/neutralizinggas.js b/test/sim/abilities/neutralizinggas.js index b2e73fe7ef9b..eef19f432e4a 100644 --- a/test/sim/abilities/neutralizinggas.js +++ b/test/sim/abilities/neutralizinggas.js @@ -375,6 +375,7 @@ describe('Neutralizing Gas', function () { const log = battle.getDebugLog(); const pressureIndex = log.indexOf('|-ability|p1b: Eternatus|Pressure'); const unnerveIndex = log.indexOf('|-ability|p2a: Rookidee|Unnerve'); + assert(unnerveIndex > 0, 'Unnerve should have an activation message'); assert(pressureIndex < unnerveIndex, 'Faster Pressure should activate before slower Unnerve'); }); }); diff --git a/test/sim/abilities/steelyspirit.js b/test/sim/abilities/steelyspirit.js index 410b629b14df..073384069b59 100644 --- a/test/sim/abilities/steelyspirit.js +++ b/test/sim/abilities/steelyspirit.js @@ -15,7 +15,7 @@ describe('Steely Spirit', function () { {species: 'aron', ability: 'steelyspirit', moves: ['ironhead']}, {species: 'aron', moves: ['ironhead']}, ], [ - {species: 'wynaut', moves: ['sleeptalk']}, + {species: 'wynaut', ability: 'shellarmor', moves: ['sleeptalk']}, {species: 'wynaut', moves: ['sleeptalk']}, ]]); @@ -34,8 +34,8 @@ describe('Steely Spirit', function () { {species: 'aron', ability: 'steelyspirit', moves: ['ironhead']}, {species: 'aron', ability: 'steelyspirit', moves: ['ironhead']}, ], [ - {species: 'wynaut', moves: ['sleeptalk']}, - {species: 'wynaut', moves: ['sleeptalk']}, + {species: 'wynaut', ability: 'shellarmor', moves: ['sleeptalk']}, + {species: 'wynaut', ability: 'shellarmor', moves: ['sleeptalk']}, ]]); battle.makeChoices('move ironhead 1, move ironhead 2', 'auto'); diff --git a/test/sim/items/mirrorherb.js b/test/sim/items/mirrorherb.js index 1994062eb66a..f75158be8b76 100644 --- a/test/sim/items/mirrorherb.js +++ b/test/sim/items/mirrorherb.js @@ -26,6 +26,7 @@ describe("Mirror Herb", () => { {species: 'Primeape', ability: 'Anger Point', moves: ['sleeptalk']}, {species: 'Gyarados', ability: 'Intimidate', moves: ['sleeptalk']}, ]]); + assert.statStage(battle.p1.active[0], 'atk', -1); battle.makeChoices(); assert.statStage(battle.p1.active[0], 'atk', -1 + 6); }); @@ -38,14 +39,14 @@ describe("Mirror Herb", () => { {species: 'Primeape', ability: 'Defiant', item: 'Weakness Policy', moves: ['sleeptalk', 'haze']}, {species: 'Annihilape', ability: 'Defiant', item: 'Weakness Policy', moves: ['sleeptalk', 'howl']}, ]]); - assert.statStage(battle.p1.active[0], 'atk', 4); + assert.statStage(battle.p1.active[0], 'atk', 4, `Mirror Herb should have copied both Defiant boosts but only boosted atk by ${battle.p1.active[0].boosts.atk}`); battle.makeChoices('auto', 'move haze, move howl'); - assert.statStage(battle.p1.active[0], 'atk', 2); + assert.statStage(battle.p1.active[0], 'atk', 2, `Mirror Herb should have copied both Howl boosts but only boosted atk by ${battle.p1.active[0].boosts.atk}`); battle.makeChoices('move recycle, move air cutter', 'auto'); - assert.statStage(battle.p1.active[0], 'spa', 4); + assert.statStage(battle.p1.active[0], 'spa', 4, `Mirror Herb should have copied all Weakness Policy boosts but only boosted spa by ${battle.p1.active[0].boosts.spa}`); }); - it.skip("should wait for most entrance abilities before copying all their (opposing) boosts", () => { + it("should wait for most entrance abilities before copying all their (opposing) boosts", () => { battle = common.createBattle({gameType: 'doubles'}, [[ {species: 'Electrode', item: 'Mirror Herb', moves: ['recycle']}, {species: 'Gyarados', ability: 'Intimidate', moves: ['sleeptalk']}, @@ -53,7 +54,6 @@ describe("Mirror Herb", () => { {species: 'Zacian', ability: 'Intrepid Sword', moves: ['sleeptalk']}, {species: 'Annihilape', ability: 'Defiant', moves: ['sleeptalk']}, ]]); - common.saveReplay(battle); assert.statStage(battle.p1.active[0], 'atk', 3); }); }); diff --git a/test/sim/items/seeds.js b/test/sim/items/seeds.js index 85d43182cf8f..ee3b7e928d7f 100644 --- a/test/sim/items/seeds.js +++ b/test/sim/items/seeds.js @@ -32,7 +32,7 @@ describe('Seeds', function () { assert.holdsItem(battle.p1.active[0]); }); - it.skip(`should activate on switching in after other entrance Abilities, at the same time as Primal reversion`, function () { + it(`should activate on switching in after other entrance Abilities, at the same time as Primal reversion`, function () { battle = common.createBattle([[ {species: 'Tapu Koko', ability: 'electricsurge', moves: ['finalgambit']}, {species: 'Groudon', ability: 'drought', item: 'redorb', moves: ['sleeptalk']}, @@ -42,8 +42,11 @@ describe('Seeds', function () { ]]); battle.makeChoices(); battle.makeChoices(); + const log = battle.getDebugLog(); const redOrbIndex = log.indexOf('Groudon-Primal'); const electricSeedIndex = log.indexOf('Electric Seed'); - assert(redOrbIndex < electricSeedIndex, 'Groudon should undergo Primal Reversion first, then Electric Seed should activate, because Groudon is faster.'); + assert(redOrbIndex > 0, 'Groudon should undergo Primal Reversion'); + assert(electricSeedIndex > 0, 'Electric Seed should activate'); + assert(redOrbIndex < electricSeedIndex, 'Groudon should undergo Primal Reversion before Electric Seed activates, because Groudon is faster.'); }); }); diff --git a/test/sim/items/whiteherb.js b/test/sim/items/whiteherb.js index f1b2329e5a6e..f2ad7b59439b 100644 --- a/test/sim/items/whiteherb.js +++ b/test/sim/items/whiteherb.js @@ -37,7 +37,7 @@ describe("White Herb", function () { assert.statStage(wynaut, 'spa', 0); }); - it.skip('should activate after two Intimidate switch in at the same time', function () { + it('should activate after two Intimidate switch in at the same time', function () { battle = common.createBattle({gameType: 'doubles'}, [[ {species: 'litten', ability: 'intimidate', moves: ['sleeptalk']}, {species: 'torracat', ability: 'intimidate', moves: ['sleeptalk', 'finalgambit']}, diff --git a/test/sim/misc/partnersincrime.js b/test/sim/misc/partnersincrime.js new file mode 100644 index 000000000000..7a04e6d123fc --- /dev/null +++ b/test/sim/misc/partnersincrime.js @@ -0,0 +1,27 @@ +'use strict'; + +const assert = require('./../../assert'); +const common = require('./../../common'); + +let battle; + +describe('Partners in Crime', function () { + afterEach(() => battle.destroy()); + + it('should activate shared abilities at the same time as other abilities', function () { + battle = common.createBattle({formatid: 'gen9partnersincrime'}, [[ + {species: 'Incineroar', ability: 'intimidate', moves: ['sleeptalk']}, + {species: 'Pincurchin', ability: 'electricsurge', moves: ['sleeptalk']}, + ], [ + {species: 'Baxcalibur', ability: 'icebody', moves: ['sleeptalk']}, + {species: 'Iron Valiant', ability: 'quarkdrive', moves: ['sleeptalk']}, + ]]); + // team preview + battle.makeChoices(); + const baxcalibur = battle.p2.active[0]; + assert.statStage(baxcalibur, 'atk', -2); + assert.equal(baxcalibur.volatiles.quarkdrive.bestStat, 'def', + `Baxcalibur should be Intimidated before Quark Drive activates`); + assert.equal(battle.field.terrainState.source.name, 'Incineroar', `Incineroar should set Electric Terrain`); + }); +}); diff --git a/test/sim/moves/tarshot.js b/test/sim/moves/tarshot.js index 42ce624ff108..b69d1c6182c6 100644 --- a/test/sim/moves/tarshot.js +++ b/test/sim/moves/tarshot.js @@ -41,7 +41,7 @@ describe('Tar Shot', function () { {species: 'wobbuffet', moves: ['tarshot']}, {species: 'wynaut', moves: ['fusionflare']}, ], [ - {species: 'tornadus', moves: ['sleeptalk']}, + {species: 'tornadus', ability: 'shellarmor', moves: ['sleeptalk']}, {species: 'thundurus', ability: 'deltastream', moves: ['sleeptalk']}, ]]); battle.makeChoices('move tarshot 1, move fusionflare 1', 'auto'); From 8481063881d9817e66c4a5721cfee335aab5c0aa Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Thu, 26 Dec 2024 17:38:05 -0600 Subject: [PATCH 04/23] Unskip a passing Eject Pack test --- test/sim/items/ejectpack.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/sim/items/ejectpack.js b/test/sim/items/ejectpack.js index 115cba335364..c01f1804b6b6 100644 --- a/test/sim/items/ejectpack.js +++ b/test/sim/items/ejectpack.js @@ -128,7 +128,7 @@ describe(`Eject Pack`, function () { assert.species(battle.p2.active[1], 'Wynaut'); }); - it.skip(`should not trigger until after all entrance abilities have resolved during simultaneous switches`, function () { + it(`should not trigger until after all entrance abilities have resolved during simultaneous switches`, function () { battle = common.createBattle({gameType: 'doubles'}, [[ {species: 'Hydreigon', ability: 'intimidate', moves: ['sleeptalk']}, {species: 'Wynaut', moves: ['sleeptalk']}, @@ -137,7 +137,6 @@ describe(`Eject Pack`, function () { {species: 'Mew', level: 1, ability: 'electricsurge', moves: ['sleeptalk']}, {species: 'Wynaut', moves: ['sleeptalk']}, ]]); - battle.makeChoices(); assert(battle.field.isWeather('sunnyday')); assert(battle.field.isTerrain('electricterrain')); assert.equal(battle.p2.requestState, 'switch'); From e112313001985cde7974e421c5c2b2dc259f75a4 Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Thu, 26 Dec 2024 18:48:38 -0600 Subject: [PATCH 05/23] Keep speed tie resolution consistent across the SwitchIn event --- sim/battle-actions.ts | 3 ++- sim/battle.ts | 13 +++++++++++++ test/sim/misc/multi-battle.js | 2 +- test/sim/misc/terastal.js | 6 +++--- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts index ef1752b82b7f..2fa3f8bd0b8f 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -175,8 +175,9 @@ export class BattleActions { const nextSwitch = this.battle.queue.shift(); switchersIn.push(nextSwitch!.pokemon!); } + this.battle.prng.shuffle(this.battle.speedTieResolution); this.battle.fieldEvent('SwitchIn', switchersIn); - + if (!pokemon.hp) return false; pokemon.isStarted = true; pokemon.draggedIn = null; diff --git a/sim/battle.ts b/sim/battle.ts index 659a9f5b3640..32e94d359da7 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -174,6 +174,7 @@ export class Battle { lastDamage: number; effectOrder: number; quickClawRoll: boolean; + speedTieResolution: number[]; teamGenerator: ReturnType | null; @@ -261,6 +262,10 @@ export class Battle { this.lastDamage = 0; this.effectOrder = 0; this.quickClawRoll = false; + this.speedTieResolution = []; + for (let i = 0; i < this.activePerHalf * 2; i++) { + this.speedTieResolution.push(i / (this.activePerHalf * 2)); + } this.teamGenerator = null; @@ -950,6 +955,14 @@ export class Battle { } if (handler.effectHolder && (handler.effectHolder as Pokemon).getStat) { handler.speed = (handler.effectHolder as Pokemon).speed; + if (callbackName.endsWith('SwitchIn')) { + // Pokemon speeds including ties are resolved before all onSwitchIn handlers and aren't re-sorted in-between + // so we add a fractional speed to each Pokemon's respective event handlers by using their unique field position + // to index a randomly shuffled array of sequential numbers + const allSlots = 'abcdef'; + const speedTieIndex = allSlots.indexOf((handler.effectHolder as Pokemon).getSlot().charAt(2)); + handler.speed += this.speedTieResolution[speedTieIndex]; + } } return handler; } diff --git a/test/sim/misc/multi-battle.js b/test/sim/misc/multi-battle.js index ac39e23a2661..ee8c9a43af9c 100644 --- a/test/sim/misc/multi-battle.js +++ b/test/sim/misc/multi-battle.js @@ -24,7 +24,7 @@ describe('Free-for-all', function () { battle.makeChoices(); battle.lose('p2'); assert(battle.p2.activeRequest.wait); - battle.makeChoices('auto', '', 'move uturn', 'auto'); + battle.makeChoices('auto', '', 'move uturn 1', 'auto'); battle.lose('p3'); battle.makeChoices(); assert.equal(battle.turn, 4); diff --git a/test/sim/misc/terastal.js b/test/sim/misc/terastal.js index e407e1d96e9d..143907bb2e7a 100644 --- a/test/sim/misc/terastal.js +++ b/test/sim/misc/terastal.js @@ -35,9 +35,9 @@ describe("Terastallization", function () { it('should give STAB correctly to the user\'s old types', function () { battle = common.createBattle([[ - {species: 'Ampharos', ability: 'static', moves: ['shockwave', 'swift'], teraType: 'Electric'}, + {species: 'Ampharos', ability: 'shellarmor', moves: ['shockwave', 'swift'], teraType: 'Electric'}, ], [ - {species: 'Ampharos', ability: 'static', moves: ['shockwave', 'swift'], teraType: 'Normal'}, + {species: 'Ampharos', ability: 'shellarmor', moves: ['shockwave', 'swift'], teraType: 'Normal'}, ]]); battle.makeChoices('move shockwave terastallize', 'move shockwave terastallize'); const teraDamage = battle.p2.active[0].maxhp - battle.p2.active[0].hp; @@ -47,7 +47,7 @@ describe("Terastallization", function () { const nonTeraDamage = battle.p1.active[0].maxhp - battle.p1.active[0].hp; // 0 SpA Ampharos Shock Wave vs. 0 HP / 0 SpD Ampharos: 40-48 assert.bounded(nonTeraDamage, [40, 48], - "Terastallizing did not keep old type's STAB; actual damage: " + teraDamage); + "Terastallizing did not keep old type's STAB; actual damage: " + nonTeraDamage); battle = common.createBattle([[ {species: 'Mimikyu', ability: 'disguise', item: 'laggingtail', moves: ['shadowclaw', 'waterfall', 'sleeptalk'], teraType: 'Water'}, From ea7433aedb6b7a709ace342d07c77cec78113413 Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Fri, 27 Dec 2024 13:05:31 -0600 Subject: [PATCH 06/23] Actually pre-sort by speed for better PRNG usage --- sim/battle-actions.ts | 39 ++++++++++++++++++-------------- sim/battle.ts | 22 +++++++++--------- test/sim/abilities/pastelveil.js | 2 +- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts index 2fa3f8bd0b8f..175289978b99 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -169,38 +169,43 @@ export class BattleActions { return true; } runSwitch(pokemon: Pokemon) { - if (this.battle.gen >= 5) { + const battle = this.battle; + if (battle.gen >= 5) { const switchersIn = [pokemon]; - for (let a = this.battle.queue.peek(); a?.choice === 'runSwitch'; a = this.battle.queue.peek()) { - const nextSwitch = this.battle.queue.shift(); + for (let a = battle.queue.peek(); a?.choice === 'runSwitch'; a = battle.queue.peek()) { + const nextSwitch = battle.queue.shift(); switchersIn.push(nextSwitch!.pokemon!); } - this.battle.prng.shuffle(this.battle.speedTieResolution); - this.battle.fieldEvent('SwitchIn', switchersIn); + const allActive = battle.getAllActive(true); + battle.speedSort(allActive); + battle.speedOrder = allActive.map((a) => a.side.n * battle.sides.length + a.position); + battle.fieldEvent('SwitchIn', switchersIn); - if (!pokemon.hp) return false; - pokemon.isStarted = true; - pokemon.draggedIn = null; + for (const poke of allActive) { + if (!poke.hp) continue; + poke.isStarted = true; + poke.draggedIn = null; + } return true; } - this.battle.runEvent('EntryHazard', pokemon); + battle.runEvent('EntryHazard', pokemon); - this.battle.runEvent('SwitchIn', pokemon); + battle.runEvent('SwitchIn', pokemon); - if (this.battle.gen <= 2) { + if (battle.gen <= 2) { // pokemon.lastMove is reset for all Pokemon on the field after a switch. This affects Mirror Move. - for (const poke of this.battle.getAllActive()) poke.lastMove = null; - if (!pokemon.side.faintedThisTurn && pokemon.draggedIn !== this.battle.turn) { - this.battle.runEvent('AfterSwitchInSelf', pokemon); + for (const poke of battle.getAllActive()) poke.lastMove = null; + if (!pokemon.side.faintedThisTurn && pokemon.draggedIn !== battle.turn) { + battle.runEvent('AfterSwitchInSelf', pokemon); } } if (!pokemon.hp) return false; pokemon.isStarted = true; if (!pokemon.fainted) { - this.battle.singleEvent('Start', pokemon.getAbility(), pokemon.abilityState, pokemon); - this.battle.singleEvent('Start', pokemon.getItem(), pokemon.itemState, pokemon); + battle.singleEvent('Start', pokemon.getAbility(), pokemon.abilityState, pokemon); + battle.singleEvent('Start', pokemon.getItem(), pokemon.itemState, pokemon); } - if (this.battle.gen === 4) { + if (battle.gen === 4) { for (const foeActive of pokemon.foes()) { foeActive.removeVolatile('substitutebroken'); } diff --git a/sim/battle.ts b/sim/battle.ts index 32e94d359da7..dd3a6b467693 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -174,7 +174,7 @@ export class Battle { lastDamage: number; effectOrder: number; quickClawRoll: boolean; - speedTieResolution: number[]; + speedOrder: number[]; teamGenerator: ReturnType | null; @@ -262,9 +262,9 @@ export class Battle { this.lastDamage = 0; this.effectOrder = 0; this.quickClawRoll = false; - this.speedTieResolution = []; + this.speedOrder = []; for (let i = 0; i < this.activePerHalf * 2; i++) { - this.speedTieResolution.push(i / (this.activePerHalf * 2)); + this.speedOrder.push(i); } this.teamGenerator = null; @@ -954,14 +954,14 @@ export class Battle { } } if (handler.effectHolder && (handler.effectHolder as Pokemon).getStat) { - handler.speed = (handler.effectHolder as Pokemon).speed; + const pokemon = (handler.effectHolder as Pokemon); + handler.speed = pokemon.speed; if (callbackName.endsWith('SwitchIn')) { // Pokemon speeds including ties are resolved before all onSwitchIn handlers and aren't re-sorted in-between - // so we add a fractional speed to each Pokemon's respective event handlers by using their unique field position - // to index a randomly shuffled array of sequential numbers - const allSlots = 'abcdef'; - const speedTieIndex = allSlots.indexOf((handler.effectHolder as Pokemon).getSlot().charAt(2)); - handler.speed += this.speedTieResolution[speedTieIndex]; + // so we subtract a fractional speed to each Pokemon's respective event handlers by using the index of their + // unique field position in a pre-sorted-by-speed array + const fieldPositionValue = pokemon.side.n * this.sides.length + pokemon.position; + handler.speed -= this.speedOrder.indexOf(fieldPositionValue) / (this.activePerHalf * 2); } } return handler; @@ -1248,11 +1248,11 @@ export class Battle { return pokemonList; } - getAllActive() { + getAllActive(includeFainted?: boolean) { const pokemonList: Pokemon[] = []; for (const side of this.sides) { for (const pokemon of side.active) { - if (pokemon && !pokemon.fainted) { + if (pokemon && (includeFainted || !pokemon.fainted)) { pokemonList.push(pokemon); } } diff --git a/test/sim/abilities/pastelveil.js b/test/sim/abilities/pastelveil.js index d685f755618c..841010cc3285 100644 --- a/test/sim/abilities/pastelveil.js +++ b/test/sim/abilities/pastelveil.js @@ -29,7 +29,7 @@ describe('Pastel Veil', function () { {species: 'wynaut', moves: ['sleeptalk']}, {species: 'wynaut', moves: ['sleeptalk']}, ], [ - {species: 'croagunk', moves: ['skillswap', 'sleeptalk']}, + {species: 'croagunk', moves: ['sleeptalk', 'skillswap']}, {species: 'wynaut', ability: 'compoundeyes', moves: ['poisongas']}, ]]); battle.makeChoices('auto', 'move skillswap 1, move poisongas'); From 212fb74c9bb4087fa9c40c2bc3208504454cb045 Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Fri, 27 Dec 2024 13:42:01 -0600 Subject: [PATCH 07/23] Split failing Commander switch out test and band-aid-fix Commander + Eject Pack --- data/abilities.ts | 2 +- test/sim/abilities/commander.js | 32 +++++++++++++++++++++++--------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/data/abilities.ts b/data/abilities.ts index 3f6518dc5ab1..7f3751eee397 100644 --- a/data/abilities.ts +++ b/data/abilities.ts @@ -602,8 +602,8 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { this.singleEvent('Update', this.effect, this.effectState, target); }, onUpdate(pokemon) { - if (this.gameType !== 'doubles' || !this.effectState.started) return; const ally = pokemon.allies()[0]; + if (this.gameType !== 'doubles' || !this.effectState.started || pokemon.switchFlag || ally?.switchFlag) return; if (!ally || pokemon.baseSpecies.baseSpecies !== 'Tatsugiri' || ally.baseSpecies.baseSpecies !== 'Dondozo') { // Handle any edge cases if (pokemon.getVolatile('commanding')) pokemon.removeVolatile('commanding'); diff --git a/test/sim/abilities/commander.js b/test/sim/abilities/commander.js index dc135bd72f50..868966856492 100644 --- a/test/sim/abilities/commander.js +++ b/test/sim/abilities/commander.js @@ -106,20 +106,14 @@ describe('Commander', function () { {species: 'wynaut', item: 'redcard', ability: 'noguard', moves: ['sleeptalk', 'tackle', 'dragontail']}, {species: 'gyarados', item: 'ejectbutton', ability: 'intimidate', moves: ['sleeptalk', 'trick', 'roar']}, ], [ - {species: 'tatsugiri', ability: 'commander', item: 'ejectpack', moves: ['sleeptalk']}, - {species: 'dondozo', item: 'ejectpack', moves: ['sleeptalk', 'peck']}, + {species: 'tatsugiri', ability: 'commander', moves: ['sleeptalk']}, + {species: 'dondozo', moves: ['sleeptalk', 'peck']}, {species: 'rufflet', moves: ['sleeptalk']}, ]]); - const tatsugiri = battle.p2.active[0]; + // const tatsugiri = battle.p2.active[0]; const dondozo = battle.p2.active[1]; - assert.statStage(tatsugiri, 'atk', -1); - assert.equal(battle.requestState, 'move', 'It should not have switched out on Eject Pack'); - assert.holdsItem(tatsugiri); - assert.statStage(dondozo, 'atk', 1); - assert.holdsItem(dondozo); - battle.makeChoices('move tackle 2, move trick 2', 'auto'); assert.holdsItem(dondozo); assert.equal(battle.requestState, 'move', 'It should not have switched out on Eject Button'); @@ -132,6 +126,26 @@ describe('Commander', function () { assert.equal(battle.requestState, 'move', 'It should not have switched out on standard phazing moves'); }); + it.skip(`should prevent Eject Pack switchouts`, function () { + battle = common.createBattle({gameType: 'doubles'}, [[ + {species: 'wynaut', item: 'redcard', ability: 'noguard', moves: ['sleeptalk', 'tackle', 'dragontail']}, + {species: 'gyarados', item: 'ejectbutton', ability: 'intimidate', moves: ['sleeptalk', 'trick', 'roar']}, + ], [ + {species: 'tatsugiri', ability: 'commander', item: 'ejectpack', moves: ['sleeptalk']}, + {species: 'dondozo', item: 'ejectpack', moves: ['sleeptalk', 'peck']}, + {species: 'rufflet', moves: ['sleeptalk']}, + ]]); + + const tatsugiri = battle.p2.active[0]; + const dondozo = battle.p2.active[1]; + + assert.statStage(tatsugiri, 'atk', -1); + assert.equal(battle.requestState, 'move', 'It should not have switched out on Eject Pack'); + assert.holdsItem(tatsugiri); + assert.statStage(dondozo, 'atk', 1); + assert.holdsItem(dondozo); + }); + it(`should cause Dondozo to stay commanded even if Tatsugiri faints`, function () { battle = common.createBattle({gameType: 'doubles'}, [[ {species: 'hypno', moves: ['sleeptalk']}, From fcbe2a9e7ac6642a3ed5af0c3c2d02cf529c753c Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Thu, 2 Jan 2025 12:34:18 -0600 Subject: [PATCH 08/23] Don't break Commander and Booster Energy if onStart is skipped --- data/abilities.ts | 3 ++- data/items.ts | 2 +- sim/battle-actions.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/data/abilities.ts b/data/abilities.ts index 7f3751eee397..3ebcf7f939b0 100644 --- a/data/abilities.ts +++ b/data/abilities.ts @@ -603,7 +603,8 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { }, onUpdate(pokemon) { const ally = pokemon.allies()[0]; - if (this.gameType !== 'doubles' || !this.effectState.started || pokemon.switchFlag || ally?.switchFlag) return; + if (this.gameType !== 'doubles' || !this.effectState.started && !pokemon.isStarted) return; + if (pokemon.switchFlag || ally?.switchFlag) return; if (!ally || pokemon.baseSpecies.baseSpecies !== 'Tatsugiri' || ally.baseSpecies.baseSpecies !== 'Dondozo') { // Handle any edge cases if (pokemon.getVolatile('commanding')) pokemon.removeVolatile('commanding'); diff --git a/data/items.ts b/data/items.ts index 27f685cc2e2d..014cd0def564 100644 --- a/data/items.ts +++ b/data/items.ts @@ -619,7 +619,7 @@ export const Items: import('../sim/dex-items').ItemDataTable = { this.singleEvent('Update', this.effect, this.effectState, pokemon); }, onUpdate(pokemon) { - if (!this.effectState.started || pokemon.transformed) return; + if (!this.effectState.started && !pokemon.isStarted || pokemon.transformed) return; if (pokemon.hasAbility('protosynthesis') && !this.field.isWeather('sunnyday') && pokemon.useItem()) { pokemon.addVolatile('protosynthesis'); diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts index 175289978b99..72c481753635 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -181,7 +181,7 @@ export class BattleActions { battle.speedOrder = allActive.map((a) => a.side.n * battle.sides.length + a.position); battle.fieldEvent('SwitchIn', switchersIn); - for (const poke of allActive) { + for (const poke of switchersIn) { if (!poke.hp) continue; poke.isStarted = true; poke.draggedIn = null; From 7980cc0825a1610b577d20093c11ffec5425325c Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Thu, 2 Jan 2025 13:02:10 -0600 Subject: [PATCH 09/23] Minimize risk of event depth overflow crashes --- data/abilities.ts | 8 ++++---- data/items.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/data/abilities.ts b/data/abilities.ts index 3ebcf7f939b0..67b311dd4fe3 100644 --- a/data/abilities.ts +++ b/data/abilities.ts @@ -91,7 +91,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { onSwitchIn(pokemon) { // Air Lock does not activate when Skill Swapped or when Neutralizing Gas leaves the field this.add('-ability', pokemon, 'Air Lock'); - this.singleEvent('Start', this.effect, this.effectState, pokemon); + ((this.effect as any).onStart as (p: Pokemon) => void).call(this, pokemon); }, onStart(pokemon) { pokemon.abilityState.ending = false; // Clear the ending flag @@ -538,7 +538,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { onSwitchIn(pokemon) { // Cloud Nine does not activate when Skill Swapped or when Neutralizing Gas leaves the field this.add('-ability', pokemon, 'Cloud Nine'); - this.singleEvent('Start', this.effect, this.effectState, pokemon); + ((this.effect as any).onStart as (p: Pokemon) => void).call(this, pokemon); }, onStart(pokemon) { pokemon.abilityState.ending = false; // Clear the ending flag @@ -597,9 +597,9 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { }, commander: { onSwitchInPriority: -2, - onStart(target) { + onStart(pokemon) { this.effectState.started = true; - this.singleEvent('Update', this.effect, this.effectState, target); + ((this.effect as any).onUpdate as (p: Pokemon) => void).call(this, pokemon); }, onUpdate(pokemon) { const ally = pokemon.allies()[0]; diff --git a/data/items.ts b/data/items.ts index 014cd0def564..3009a36b833c 100644 --- a/data/items.ts +++ b/data/items.ts @@ -616,7 +616,7 @@ export const Items: import('../sim/dex-items').ItemDataTable = { onSwitchInPriority: -2, onStart(pokemon) { this.effectState.started = true; - this.singleEvent('Update', this.effect, this.effectState, pokemon); + ((this.effect as any).onUpdate as (p: Pokemon) => void).call(this, pokemon); }, onUpdate(pokemon) { if (!this.effectState.started && !pokemon.isStarted || pokemon.transformed) return; @@ -7193,7 +7193,7 @@ export const Items: import('../sim/dex-items').ItemDataTable = { }, onSwitchInPriority: -2, onStart(pokemon) { - this.singleEvent('Update', this.effect, this.effectState, pokemon); + ((this.effect as any).onUpdate as (p: Pokemon) => void).call(this, pokemon); }, onUpdate(pokemon) { let activate = false; From 918ebdefb6016bc27222ab9d0ef71e5e94c81a49 Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Thu, 2 Jan 2025 21:16:48 -0600 Subject: [PATCH 10/23] Fix unintended changes to speed ties in other events --- sim/battle.ts | 10 +++++++++- sim/pokemon.ts | 6 ++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/sim/battle.ts b/sim/battle.ts index dd3a6b467693..3fa5fec78385 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -94,6 +94,7 @@ interface EventListener extends EventListenerWithoutPriority { order: number | false; priority: number; subOrder: number; + effectOrder?: number; speed?: number; } @@ -407,7 +408,7 @@ export class Battle { ((b.priority || 0) - (a.priority || 0)) || ((b.speed || 0) - (a.speed || 0)) || -((b.subOrder || 0) - (a.subOrder || 0)) || - -((b.state?.effectOrder || 0) - (a.state?.effectOrder || 0)) || + -((b.effectOrder || 0) - (a.effectOrder || 0)) || 0; } @@ -953,6 +954,13 @@ export class Battle { } } } + if (callbackName.endsWith('SwitchIn') || callbackName.endsWith('RedirectTarget')) { + // If multiple hazards are present on one side, their event handlers all perfectly tie in speed, priority, + // and subOrder. They should activate in the order they were created, which is where effectOrder comes in. + // This also applies to speed ties for which ability like Lightning Rod redirects moves. + // TODO: In-game, other events are also sorted this way, but that's an implementation for another refactor + handler.effectOrder = handler.state?.effectOrder; + } if (handler.effectHolder && (handler.effectHolder as Pokemon).getStat) { const pokemon = (handler.effectHolder as Pokemon); handler.speed = pokemon.speed; diff --git a/sim/pokemon.ts b/sim/pokemon.ts index 6f250296bd1e..49e22e2c3037 100644 --- a/sim/pokemon.ts +++ b/sim/pokemon.ts @@ -36,10 +36,12 @@ export class EffectState { duration?: number; [k: string]: any; - constructor(data: AnyObject, battle: Battle) { + constructor(data: AnyObject, battle: Battle, effectOrder?: number) { this.id = data.id || ''; Object.assign(this, data); - if (this.id && this.target && (!(this.target instanceof Pokemon) || this.target.isActive)) { + if (effectOrder !== undefined) { + this.effectOrder = effectOrder; + } else if (this.id && this.target && (!(this.target instanceof Pokemon) || this.target.isActive)) { this.effectOrder = battle.effectOrder++; } else { this.effectOrder = 0; From ce522f8df09555f041ceace11cf0a9d6f84680c8 Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Thu, 2 Jan 2025 21:16:58 -0600 Subject: [PATCH 11/23] Fix serialization --- sim/state.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/sim/state.ts b/sim/state.ts index 0a4fcdef4935..1fd5dfc02b4a 100644 --- a/sim/state.ts +++ b/sim/state.ts @@ -292,6 +292,23 @@ export const State = new class { return move; } + // EffectState is a class in the first place so its constructor can manage effectOrder + // the Battle object itself also has an effectOrder property, though, so we need to filter that out + isEffectState(obj: AnyObject): obj is EffectState { + return obj.hasOwnProperty('effectOrder') && !obj.hasOwnProperty('prngSeed'); + } + + serializeEffectState(effectState: EffectState, battle: Battle): /* EffectState */ AnyObject { + return this.serialize(effectState, new Set(['clear']), battle); + } + + deserializeEffectState(state: /* EffectState */ AnyObject, battle: Battle): EffectState { + const effectOrder: EffectState['effectOrder'] = state.effectOrder; + delete state.effectOrder; + const effectState = new EffectState(this.deserializeWithRefs(state, battle), battle, effectOrder); + return effectState; + } + serializeWithRefs(obj: unknown, battle: Battle): unknown { switch (typeof obj) { case 'function': @@ -312,6 +329,7 @@ export const State = new class { } if (this.isActiveMove(obj)) return this.serializeActiveMove(obj, battle); + if (this.isEffectState(obj)) return this.serializeEffectState(obj, battle); if (this.isReferable(obj)) return this.toRef(obj); if (obj.constructor !== Object) { // If we're getting this error, some 'special' field has been added to @@ -352,6 +370,7 @@ export const State = new class { } if (this.isActiveMove(obj)) return this.deserializeActiveMove(obj, battle); + if (this.isEffectState(obj)) return this.deserializeEffectState(obj, battle); const o: any = {}; for (const [key, value] of Object.entries(obj)) { @@ -368,8 +387,8 @@ export const State = new class { // NOTE: see explanation on the declaration above for why this must be defined lazily. if (!this.REFERABLE) { this.REFERABLE = new Set([ - Battle, Field, Side, Pokemon, EffectState, - Dex.Condition, Dex.Ability, Dex.Item, Dex.Move, Dex.Species, + Battle, Field, Side, Pokemon, Dex.Condition, + Dex.Ability, Dex.Item, Dex.Move, Dex.Species, ]); } return this.REFERABLE.has(obj.constructor); From fe0e77b0abd10a0463b1c3d8e610a2c0ad3a73b2 Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Sun, 19 Jan 2025 16:50:16 -0600 Subject: [PATCH 12/23] Fix White Herb timing when holder is not the one switching in --- data/items.ts | 5 ++++- sim/pokemon.ts | 4 ++-- test/sim/items/whiteherb.js | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/data/items.ts b/data/items.ts index 3009a36b833c..8de714269fee 100644 --- a/data/items.ts +++ b/data/items.ts @@ -7191,7 +7191,10 @@ export const Items: import('../sim/dex-items').ItemDataTable = { } }, }, - onSwitchInPriority: -2, + onAnySwitchInPriority: -2, + onAnySwitchIn() { + ((this.effect as any).onUpdate as (p: Pokemon) => void).call(this, this.effectState.target); + }, onStart(pokemon) { ((this.effect as any).onUpdate as (p: Pokemon) => void).call(this, pokemon); }, diff --git a/sim/pokemon.ts b/sim/pokemon.ts index 49e22e2c3037..00f526a504fe 100644 --- a/sim/pokemon.ts +++ b/sim/pokemon.ts @@ -425,10 +425,10 @@ export class Pokemon { this.baseAbility = toID(set.ability); this.ability = this.baseAbility; - this.abilityState = new EffectState({id: this.ability}, this.battle); + this.abilityState = new EffectState({id: this.ability, target: this}, this.battle); this.item = toID(set.item); - this.itemState = new EffectState({id: this.item}, this.battle); + this.itemState = new EffectState({id: this.item, target: this}, this.battle); this.lastItem = ''; this.usedItemThisTurn = false; this.ateBerry = false; diff --git a/test/sim/items/whiteherb.js b/test/sim/items/whiteherb.js index f2ad7b59439b..2806edc1655e 100644 --- a/test/sim/items/whiteherb.js +++ b/test/sim/items/whiteherb.js @@ -60,4 +60,22 @@ describe("White Herb", function () { assert.false.holdsItem(wynaut); assert.statStage(wynaut, 'atk', 0); }); + + it('should activate before Opportunist during switch-ins', function () { + battle = common.createBattle({gameType: 'doubles'}, [[ + {species: 'axew', moves: ['sleeptalk']}, + {species: 'fraxure', moves: ['finalgambit']}, + {species: 'zacian', ability: 'intrepidsword', moves: ['sleeptalk']}, + {species: 'torracat', ability: 'intimidate', moves: ['sleeptalk']}, + ], [ + {species: 'flittle', item: 'whiteherb', ability: 'opportunist', moves: ['sleeptalk']}, + {species: 'haxorus', moves: ['sleeptalk']}, + ]]); + battle.makeChoices('move sleeptalk, move finalgambit -1', 'auto'); + battle.makeChoices('switch 3, switch 4'); + const flittle = battle.p2.active[0]; + assert.false.holdsItem(flittle); + assert.statStage(flittle, 'atk', 1); + common.saveReplay(battle); + }); }); From a460633a1b752efc3ab01aa62eccf5f04f1a59ac Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Sun, 19 Jan 2025 17:33:27 -0600 Subject: [PATCH 13/23] Fix Commander/Pastel Veil when holder is not the one switching in --- data/abilities.ts | 13 +++++++------ sim/dex-abilities.ts | 2 +- sim/dex-conditions.ts | 1 - sim/dex-items.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/data/abilities.ts b/data/abilities.ts index 67b311dd4fe3..a347b6ff5f36 100644 --- a/data/abilities.ts +++ b/data/abilities.ts @@ -596,7 +596,11 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 213, }, commander: { - onSwitchInPriority: -2, + onAnySwitchInPriority: -2, + onAnySwitchIn() { + this.effectState.started = true; + ((this.effect as any).onUpdate as (p: Pokemon) => void).call(this, this.effectState.target); + }, onStart(pokemon) { this.effectState.started = true; ((this.effect as any).onUpdate as (p: Pokemon) => void).call(this, pokemon); @@ -3113,11 +3117,8 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { pokemon.cureStatus(); } }, - onAllySwitchIn(pokemon) { - if (['psn', 'tox'].includes(pokemon.status)) { - this.add('-activate', this.effectState.target, 'ability: Pastel Veil'); - pokemon.cureStatus(); - } + onAnySwitchIn() { + ((this.effect as any).onStart as (p: Pokemon) => void).call(this, this.effectState.target); }, onSetStatus(status, target, source, effect) { if (!['psn', 'tox'].includes(status.id)) return; diff --git a/sim/dex-abilities.ts b/sim/dex-abilities.ts index 6e8c2d6d72ea..8f5f03feb1d6 100644 --- a/sim/dex-abilities.ts +++ b/sim/dex-abilities.ts @@ -97,7 +97,7 @@ export class DexAbilities { // Abilities and items Start at different times during the SwitchIn event, so we do this // instead of running the Start event during switch-ins // gens 4 and before still use the old system, though - if (abilityData.onStart && !abilityData.onSwitchIn) { + if (abilityData.onStart && !abilityData.onSwitchIn && !abilityData.onAnySwitchIn) { abilityData.onSwitchIn = abilityData.onStart; } } diff --git a/sim/dex-conditions.ts b/sim/dex-conditions.ts index 08b2d1ec0919..f8c160c98046 100644 --- a/sim/dex-conditions.ts +++ b/sim/dex-conditions.ts @@ -562,7 +562,6 @@ export interface PokemonEventMethods extends EventMethods { onAllySetWeather?: (this: Battle, target: Pokemon, source: Pokemon, weather: Condition) => boolean | void; onAllySideConditionStart?: (this: Battle, target: Pokemon, source: Pokemon, sideCondition: Condition) => void; onAllyStallMove?: (this: Battle, pokemon: Pokemon) => boolean | void; - onAllySwitchIn?: (this: Battle, pokemon: Pokemon) => void; onAllySwitchOut?: (this: Battle, pokemon: Pokemon) => void; onAllyTakeItem?: ( (this: Battle, item: Item, pokemon: Pokemon, source: Pokemon, move?: ActiveMove) => boolean | void diff --git a/sim/dex-items.ts b/sim/dex-items.ts index 28ba571054ee..f0bbf26ae466 100644 --- a/sim/dex-items.ts +++ b/sim/dex-items.ts @@ -197,7 +197,7 @@ export class DexItems { // Abilities and items Start at different times during the SwitchIn event, so we do this // instead of running the Start event during switch-ins // gens 4 and before still use the old system, though - if (itemData.onStart && !itemData.onSwitchIn) { + if (itemData.onStart && !itemData.onSwitchIn && !itemData.onAnySwitchIn) { itemData.onSwitchIn = itemData.onStart; } } From c38793490bfd5c26a5393f74f6bbfaa827ad0898 Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Sun, 19 Jan 2025 19:05:55 -0600 Subject: [PATCH 14/23] Move EffectState initialization handling to Battle functions --- config/formats.ts | 12 +++---- data/items.ts | 4 +-- data/mods/partnersincrime/moves.ts | 6 ++-- data/mods/partnersincrime/scripts.ts | 3 +- data/mods/sharingiscaring/items.ts | 4 +-- data/mods/sharingiscaring/scripts.ts | 8 ++--- data/moves.ts | 6 ++-- sim/battle.ts | 35 ++++++++++++++++--- sim/field.ts | 18 +++++----- sim/pokemon.ts | 51 +++++++--------------------- sim/side.ts | 8 ++--- sim/state.ts | 21 +----------- 12 files changed, 75 insertions(+), 101 deletions(-) diff --git a/config/formats.ts b/config/formats.ts index b142f513b685..2035e2a38976 100644 --- a/config/formats.ts +++ b/config/formats.ts @@ -17,8 +17,6 @@ New sections will be added to the bottom of the specified column. The column value will be ignored for repeat sections. */ -import {EffectState} from '../sim/pokemon'; - export const Formats: import('../sim/dex-formats').FormatList = [ // S/V Singles @@ -730,7 +728,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ if (!format.getSharedPower) format = this.dex.formats.get('gen9sharedpower'); for (const ability of format.getSharedPower!(pokemon)) { const effect = 'ability:' + ability; - pokemon.volatiles[effect] = new EffectState({id: this.toID(effect), target: pokemon}, this); + pokemon.volatiles[effect] = this.initEffectState({id: this.toID(effect), target: pokemon}); if (!pokemon.m.abils) pokemon.m.abils = []; if (!pokemon.m.abils.includes(effect)) pokemon.m.abils.push(effect); } @@ -1464,14 +1462,14 @@ export const Formats: import('../sim/dex-formats').FormatList = [ if (!pokemon.m.innate && !BAD_ABILITIES.includes(this.toID(ally.ability))) { pokemon.m.innate = 'ability:' + ally.ability; if (!ngas || ally.getAbility().flags['cantsuppress'] || pokemon.hasItem('Ability Shield')) { - pokemon.volatiles[pokemon.m.innate] = new EffectState({id: pokemon.m.innate, target: pokemon}, this); + pokemon.volatiles[pokemon.m.innate] = this.initEffectState({id: pokemon.m.innate, target: pokemon}); pokemon.m.startVolatile = true; } } if (!ally.m.innate && !BAD_ABILITIES.includes(this.toID(pokemon.ability))) { ally.m.innate = 'ability:' + pokemon.ability; if (!ngas || pokemon.getAbility().flags['cantsuppress'] || ally.hasItem('Ability Shield')) { - ally.volatiles[ally.m.innate] = new EffectState({id: ally.m.innate, target: ally}, this); + ally.volatiles[ally.m.innate] = this.initEffectState({id: ally.m.innate, target: ally}); ally.m.startVolatile = true; } } @@ -1769,7 +1767,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ for (const item of format.getSharedItems!(pokemon)) { if (pokemon.m.sharedItemsUsed.includes(item)) continue; const effect = 'item:' + item; - pokemon.volatiles[effect] = new EffectState({id: this.toID(effect), target: pokemon}, this); + pokemon.volatiles[effect] = this.initEffectState({id: this.toID(effect), target: pokemon}); if (!pokemon.m.items) pokemon.m.items = []; if (!pokemon.m.items.includes(effect)) pokemon.m.items.push(effect); } @@ -2587,7 +2585,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ if (!format.getSharedPower) format = this.dex.formats.get('gen9sharedpower'); for (const ability of format.getSharedPower!(pokemon)) { const effect = 'ability:' + ability; - pokemon.volatiles[effect] = new EffectState({id: this.toID(effect), target: pokemon}, this); + pokemon.volatiles[effect] = this.initEffectState({id: this.toID(effect), target: pokemon}); if (!pokemon.m.abils) pokemon.m.abils = []; if (!pokemon.m.abils.includes(effect)) pokemon.m.abils.push(effect); } diff --git a/data/items.ts b/data/items.ts index 8de714269fee..ff03ac8d89f7 100644 --- a/data/items.ts +++ b/data/items.ts @@ -193,7 +193,7 @@ export const Items: import('../sim/dex-items').ItemDataTable = { onDamagingHit(damage, target, source, move) { this.add('-enditem', target, 'Air Balloon'); target.item = ''; - target.itemState.clear(); + this.clearEffectState(target.itemState); this.runEvent('AfterUseItem', target, null, null, this.dex.items.get('airballoon')); }, onAfterSubDamage(damage, target, source, effect) { @@ -201,7 +201,7 @@ export const Items: import('../sim/dex-items').ItemDataTable = { if (effect.effectType === 'Move') { this.add('-enditem', target, 'Air Balloon'); target.item = ''; - target.itemState.clear(); + this.clearEffectState(target.itemState); this.runEvent('AfterUseItem', target, null, null, this.dex.items.get('airballoon')); } }, diff --git a/data/mods/partnersincrime/moves.ts b/data/mods/partnersincrime/moves.ts index 33393bdccf6b..06628842f26f 100644 --- a/data/mods/partnersincrime/moves.ts +++ b/data/mods/partnersincrime/moves.ts @@ -1,5 +1,3 @@ -import {EffectState} from '../../../sim/pokemon'; - export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { gastroacid: { inherit: true, @@ -153,7 +151,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { } source.ability = targetAbility.id; - source.abilityState = new EffectState({id: this.toID(source.ability), target: source}, this); + source.abilityState = this.initEffectState({id: this.toID(source.ability), target: source}); if (source.m.innate && source.m.innate.endsWith(targetAbility.id)) { source.removeVolatile(source.m.innate); delete source.m.innate; @@ -168,7 +166,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { } target.ability = sourceAbility.id; - target.abilityState = new EffectState({id: this.toID(target.ability), target: target}, this); + target.abilityState = this.initEffectState({id: this.toID(target.ability), target: target}); if (target.m.innate && target.m.innate.endsWith(sourceAbility.id)) { target.removeVolatile(target.m.innate); delete target.m.innate; diff --git a/data/mods/partnersincrime/scripts.ts b/data/mods/partnersincrime/scripts.ts index 9009eab8e35b..9088cddc9a0a 100644 --- a/data/mods/partnersincrime/scripts.ts +++ b/data/mods/partnersincrime/scripts.ts @@ -1,5 +1,4 @@ import {Utils} from '../../../lib'; -import {EffectState} from '../../../sim/pokemon'; export const Scripts: ModdedBattleScriptsData = { gen: 9, @@ -240,7 +239,7 @@ export const Scripts: ModdedBattleScriptsData = { this.battle.dex.moves.get(this.battle.effect.id)); } this.ability = ability.id; - this.abilityState = new EffectState({id: ability.id, target: this}, this.battle); + this.abilityState = this.battle.initEffectState({id: ability.id, target: this}); if (ability.id && this.battle.gen > 3) { this.battle.singleEvent('Start', ability, this.abilityState, this, source); if (ally && ally.ability !== this.ability) { diff --git a/data/mods/sharingiscaring/items.ts b/data/mods/sharingiscaring/items.ts index bf0a4098e875..2075130fa1a7 100644 --- a/data/mods/sharingiscaring/items.ts +++ b/data/mods/sharingiscaring/items.ts @@ -6,7 +6,7 @@ export const Items: import('../../../sim/dex-items').ModdedItemDataTable = { this.add('-enditem', target, 'Air Balloon'); if (target.item === 'airballoon') { target.item = ''; - target.itemState.clear(); + this.clearEffectState(target.itemState); } else { delete target.volatiles['item:airballoon']; target.m.sharedItemsUsed.push('airballoon'); @@ -19,7 +19,7 @@ export const Items: import('../../../sim/dex-items').ModdedItemDataTable = { this.add('-enditem', target, 'Air Balloon'); if (target.item === 'airballoon') { target.item = ''; - target.itemState.clear(); + this.clearEffectState(target.itemState); } else { delete target.volatiles['item:airballoon']; target.m.sharedItemsUsed.push('airballoon'); diff --git a/data/mods/sharingiscaring/scripts.ts b/data/mods/sharingiscaring/scripts.ts index 206abcbb7bb8..38a941f5d73b 100644 --- a/data/mods/sharingiscaring/scripts.ts +++ b/data/mods/sharingiscaring/scripts.ts @@ -1,4 +1,4 @@ -import {EffectState, RESTORATIVE_BERRIES} from '../../../sim/pokemon'; +import {RESTORATIVE_BERRIES} from "../../../sim/pokemon"; export const Scripts: ModdedBattleScriptsData = { gen: 9, @@ -60,7 +60,7 @@ export const Scripts: ModdedBattleScriptsData = { } else { this.lastItem = this.item; this.item = ''; - this.itemState.clear(); + this.battle.clearEffectState(this.itemState); } this.usedItemThisTurn = true; this.battle.runEvent('AfterUseItem', this, null, null, item); @@ -104,7 +104,7 @@ export const Scripts: ModdedBattleScriptsData = { } else { this.lastItem = this.item; this.item = ''; - this.itemState.clear(); + this.battle.clearEffectState(this.itemState); } this.usedItemThisTurn = true; this.ateBerry = true; @@ -129,7 +129,7 @@ export const Scripts: ModdedBattleScriptsData = { const oldItem = this.getItem(); const oldItemState = this.itemState; this.item = item.id; - this.itemState = new EffectState({id: item.id, target: this}, this.battle); + this.itemState = this.battle.initEffectState({id: item.id, target: this}); if (oldItem.exists) this.battle.singleEvent('End', oldItem, oldItemState, this); if (item.id) { this.battle.singleEvent('Start', item, this.itemState, this, source, effect); diff --git a/data/moves.ts b/data/moves.ts index 349d3369d81d..f90485ffef45 100644 --- a/data/moves.ts +++ b/data/moves.ts @@ -1,7 +1,5 @@ // List of flags and their descriptions can be found in sim/dex-moves.ts -import {EffectState} from '../sim/pokemon'; - export const Moves: import('../sim/dex-moves').MoveDataTable = { "10000000voltthunderbolt": { num: 719, @@ -17223,8 +17221,8 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { this.singleEvent('End', targetAbility, target.abilityState, target); source.ability = targetAbility.id; target.ability = sourceAbility.id; - source.abilityState = new EffectState({id: this.toID(source.ability), target: source}, this); - target.abilityState = new EffectState({id: this.toID(target.ability), target: target}, this); + source.abilityState = this.initEffectState({id: this.toID(source.ability), target: source}); + target.abilityState = this.initEffectState({id: this.toID(target.ability), target: target}); source.volatileStaleness = undefined; if (!target.isAlly(source)) target.volatileStaleness = 'external'; this.singleEvent('Start', targetAbility, source.abilityState, source); diff --git a/sim/battle.ts b/sim/battle.ts index 3fa5fec78385..d9a900eb515d 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -217,7 +217,7 @@ export class Battle { options.forceRandomChance : null; this.deserialized = !!options.deserialized; this.strictChoices = !!options.strictChoices; - this.formatData = new EffectState({id: format.id}, this); + this.formatData = this.initEffectState({id: format.id}); this.gameType = (format.gameType || 'singles'); this.field = new Field(this); this.sides = Array(format.playerCount).fill(null) as any; @@ -247,7 +247,7 @@ export class Battle { this.ended = false; this.effect = {id: ''} as Effect; - this.effectState = new EffectState({id: ''}, this); + this.effectState = this.initEffectState({id: ''}); this.event = {id: ''}; this.events = null; @@ -592,7 +592,7 @@ export class Battle { const parentEvent = this.event; this.effect = effect; - this.effectState = state as EffectState || new EffectState({}, this); + this.effectState = state as EffectState || this.initEffectState({}); this.event = {id: eventid, target, source, effect: sourceEffect}; this.eventDepth++; @@ -745,7 +745,7 @@ export class Battle { if (callback !== undefined) { if (Array.isArray(target)) throw new Error(""); handlers.unshift(this.resolvePriority({ - effect: sourceEffect, callback, state: new EffectState({}, this), end: null, effectHolder: target, + effect: sourceEffect, callback, state: this.initEffectState({}), end: null, effectHolder: target, }, `on${eventid}`)); } } @@ -858,7 +858,7 @@ export class Battle { const parentEffect = this.effect; const parentEffectState = this.effectState; this.effect = handler.effect; - this.effectState = handler.state || new EffectState({}, this); + this.effectState = handler.state || this.initEffectState({}); this.effectState.target = effectHolder; returnVal = handler.callback.apply(this, args); @@ -3225,6 +3225,31 @@ export class Battle { return this.gen >= 8 ? (this.turn - 1) % 256 : this.turn - 1; } + initEffectState(obj: Partial, effectOrder?: number): EffectState { + if (!obj.id) obj.id = ''; + if (effectOrder !== undefined) { + obj.effectOrder = effectOrder; + } else if (obj.id && obj.target && (!(obj.target instanceof Pokemon) || obj.target.isActive)) { + obj.effectOrder = this.effectOrder++; + } else { + obj.effectOrder = 0; + } + return obj as EffectState; + } + + clearEffectState(state: EffectState) { + state.id = ''; + for (const k in state) { + if (k === 'id' || k === 'target') { + continue; + } else if (k === 'effectOrder') { + state.effectOrder = 0; + } else { + delete state[k]; + } + } + } + destroy() { // deallocate ourself diff --git a/sim/field.ts b/sim/field.ts index 7a972ed2b8c9..58862730ad3a 100644 --- a/sim/field.ts +++ b/sim/field.ts @@ -26,9 +26,9 @@ export class Field { this.id = ''; this.weather = ''; - this.weatherState = new EffectState({id: ''}, battle); + this.weatherState = this.battle.initEffectState({id: ''}); this.terrain = ''; - this.terrainState = new EffectState({id: ''}, battle); + this.terrainState = this.battle.initEffectState({id: ''}); this.pseudoWeather = {}; } @@ -67,7 +67,7 @@ export class Field { const prevWeather = this.weather; const prevWeatherState = this.weatherState; this.weather = status.id; - this.weatherState = new EffectState({id: status.id}, this.battle); + this.weatherState = this.battle.initEffectState({id: status.id}); if (source) { this.weatherState.source = source; this.weatherState.sourceSlot = source.getSlot(); @@ -93,7 +93,7 @@ export class Field { const prevWeather = this.getWeather(); this.battle.singleEvent('FieldEnd', prevWeather, this.weatherState, this); this.weather = ''; - this.weatherState.clear(); + this.battle.clearEffectState(this.weatherState); this.battle.eachEvent('WeatherChange'); return true; } @@ -138,12 +138,12 @@ export class Field { const prevTerrain = this.terrain; const prevTerrainState = this.terrainState; this.terrain = status.id; - this.terrainState = new EffectState({ + this.terrainState = this.battle.initEffectState({ id: status.id, source, sourceSlot: source.getSlot(), duration: status.duration, - }, this.battle); + }); if (status.durationCallback) { this.terrainState.duration = status.durationCallback.call(this.battle, source, source, sourceEffect); } @@ -161,7 +161,7 @@ export class Field { const prevTerrain = this.getTerrain(); this.battle.singleEvent('FieldEnd', prevTerrain, this.terrainState, this); this.terrain = ''; - this.terrainState.clear(); + this.battle.clearEffectState(this.terrainState); this.battle.eachEvent('TerrainChange'); return true; } @@ -197,12 +197,12 @@ export class Field { if (!(status as any).onFieldRestart) return false; return this.battle.singleEvent('FieldRestart', status, state, this, source, sourceEffect); } - state = this.pseudoWeather[status.id] = new EffectState({ + state = this.pseudoWeather[status.id] = this.battle.initEffectState({ id: status.id, source, sourceSlot: source?.getSlot(), duration: status.duration, - }, this.battle); + }); if (status.durationCallback) { if (!source) throw new Error(`setting fieldcond without a source`); state.duration = status.durationCallback.call(this.battle, source, source, sourceEffect); diff --git a/sim/pokemon.ts b/sim/pokemon.ts index 00f526a504fe..f97d9eaa725e 100644 --- a/sim/pokemon.ts +++ b/sim/pokemon.ts @@ -30,36 +30,11 @@ interface Attacker { damageValue?: (number | boolean | undefined); } -export class EffectState { +export interface EffectState { id: string; effectOrder: number; duration?: number; [k: string]: any; - - constructor(data: AnyObject, battle: Battle, effectOrder?: number) { - this.id = data.id || ''; - Object.assign(this, data); - if (effectOrder !== undefined) { - this.effectOrder = effectOrder; - } else if (this.id && this.target && (!(this.target instanceof Pokemon) || this.target.isActive)) { - this.effectOrder = battle.effectOrder++; - } else { - this.effectOrder = 0; - } - } - - clear() { - this.id = ''; - for (const k in this) { - if (k === 'id' || k === 'target') { - continue; - } else if (k === 'effectOrder') { - this.effectOrder = 0; - } else { - delete this[k]; - } - } - } } // Berries which restore PP/HP and thus inflict external staleness when given to an opponent as @@ -334,7 +309,7 @@ export class Pokemon { if (set.name === set.species || !set.name) { set.name = this.baseSpecies.baseSpecies; } - this.speciesState = new EffectState({id: this.species.id}, this.battle); + this.speciesState = this.battle.initEffectState({id: this.species.id}); this.name = set.name.substr(0, 20); this.fullname = this.side.id + ': ' + this.name; @@ -382,7 +357,7 @@ export class Pokemon { (this.gender === '' ? '' : ', ' + this.gender) + (this.set.shiny ? ', shiny' : ''); this.status = ''; - this.statusState = new EffectState({}, this.battle); + this.statusState = this.battle.initEffectState({}); this.volatiles = {}; this.showCure = undefined; @@ -425,10 +400,10 @@ export class Pokemon { this.baseAbility = toID(set.ability); this.ability = this.baseAbility; - this.abilityState = new EffectState({id: this.ability, target: this}, this.battle); + this.abilityState = this.battle.initEffectState({id: this.ability, target: this}); this.item = toID(set.item); - this.itemState = new EffectState({id: this.item, target: this}, this.battle); + this.itemState = this.battle.initEffectState({id: this.item, target: this}); this.lastItem = ''; this.usedItemThisTurn = false; this.ateBerry = false; @@ -1220,7 +1195,7 @@ export class Pokemon { if (switchCause === 'shedtail' && i !== 'substitute') continue; if (this.battle.dex.conditions.getByID(i as ID).noCopy) continue; // shallow clones - this.volatiles[i] = new EffectState(pokemon.volatiles[i], this.battle); + this.volatiles[i] = this.battle.initEffectState({...pokemon.volatiles[i]}); if (this.volatiles[i].linkedPokemon) { delete pokemon.volatiles[i].linkedPokemon; delete pokemon.volatiles[i].linkedStatus; @@ -1686,7 +1661,7 @@ export class Pokemon { } this.status = status.id; - this.statusState = new EffectState({id: status.id, target: this}, this.battle); + this.statusState = this.battle.initEffectState({id: status.id, target: this}); if (source) this.statusState.source = source; if (status.duration) this.statusState.duration = status.duration; if (status.durationCallback) { @@ -1752,7 +1727,7 @@ export class Pokemon { this.lastItem = this.item; this.item = ''; - this.itemState.clear(); + this.battle.clearEffectState(this.itemState); this.usedItemThisTurn = true; this.ateBerry = true; this.battle.runEvent('AfterUseItem', this, null, null, item); @@ -1789,7 +1764,7 @@ export class Pokemon { this.lastItem = this.item; this.item = ''; - this.itemState.clear(); + this.battle.clearEffectState(this.itemState); this.usedItemThisTurn = true; this.battle.runEvent('AfterUseItem', this, null, null, item); return true; @@ -1809,7 +1784,7 @@ export class Pokemon { if (this.battle.runEvent('TakeItem', this, source, null, item)) { this.item = ''; const oldItemState = this.itemState; - this.itemState.clear(); + this.battle.clearEffectState(this.itemState); this.pendingStaleness = undefined; this.battle.singleEvent('End', item, oldItemState, this); this.battle.runEvent('AfterTakeItem', this, null, null, item); @@ -1834,7 +1809,7 @@ export class Pokemon { const oldItem = this.getItem(); const oldItemState = this.itemState; this.item = item.id; - this.itemState = new EffectState({id: item.id, target: this}, this.battle); + this.itemState = this.battle.initEffectState({id: item.id, target: this}); if (oldItem.exists) this.battle.singleEvent('End', oldItem, oldItemState, this); if (item.id) { this.battle.singleEvent('Start', item, this.itemState, this, source, effect); @@ -1876,7 +1851,7 @@ export class Pokemon { this.battle.dex.moves.get(this.battle.effect.id)); } this.ability = ability.id; - this.abilityState = new EffectState({id: ability.id, target: this}, this.battle); + this.abilityState = this.battle.initEffectState({id: ability.id, target: this}); if (ability.id && this.battle.gen > 3 && (!isTransform || oldAbility !== ability.id || this.battle.gen <= 4)) { this.battle.singleEvent('Start', ability, this.abilityState, this, source); @@ -1935,7 +1910,7 @@ export class Pokemon { this.battle.debug('add volatile [' + status.id + '] interrupted'); return result; } - this.volatiles[status.id] = new EffectState({id: status.id, name: status.name, target: this}, this.battle); + this.volatiles[status.id] = this.battle.initEffectState({id: status.id, name: status.name, target: this}); if (source) { this.volatiles[status.id].source = source; this.volatiles[status.id].sourceSlot = source.getSlot(); diff --git a/sim/side.ts b/sim/side.ts index 491b00c5b71f..ba8ecb972aef 100644 --- a/sim/side.ts +++ b/sim/side.ts @@ -294,13 +294,13 @@ export class Side { if (!(status as any).onSideRestart) return false; return this.battle.singleEvent('SideRestart', status, this.sideConditions[status.id], this, source, sourceEffect); } - this.sideConditions[status.id] = new EffectState({ + this.sideConditions[status.id] = this.battle.initEffectState({ id: status.id, target: this, source, sourceSlot: source.getSlot(), duration: status.duration, - }, this.battle); + }); if (status.durationCallback) { this.sideConditions[status.id].duration = status.durationCallback.call(this.battle, this.active[0], source, sourceEffect); @@ -346,14 +346,14 @@ export class Side { if (!status.onRestart) return false; return this.battle.singleEvent('Restart', status, this.slotConditions[target][status.id], this, source, sourceEffect); } - const conditionState = this.slotConditions[target][status.id] = new EffectState({ + const conditionState = this.slotConditions[target][status.id] = this.battle.initEffectState({ id: status.id, target: this, source, sourceSlot: source.getSlot(), isSlotCondition: true, duration: status.duration, - }, this.battle); + }); if (status.durationCallback) { conditionState.duration = status.durationCallback.call(this.battle, this.active[0], source, sourceEffect); diff --git a/sim/state.ts b/sim/state.ts index 1fd5dfc02b4a..7a96d6024385 100644 --- a/sim/state.ts +++ b/sim/state.ts @@ -12,7 +12,7 @@ import {Battle} from './battle'; import {Dex} from './dex'; import {Field} from './field'; -import {EffectState, Pokemon} from './pokemon'; +import {Pokemon} from './pokemon'; import {PRNG} from './prng'; import {Choice, Side} from './side'; @@ -292,23 +292,6 @@ export const State = new class { return move; } - // EffectState is a class in the first place so its constructor can manage effectOrder - // the Battle object itself also has an effectOrder property, though, so we need to filter that out - isEffectState(obj: AnyObject): obj is EffectState { - return obj.hasOwnProperty('effectOrder') && !obj.hasOwnProperty('prngSeed'); - } - - serializeEffectState(effectState: EffectState, battle: Battle): /* EffectState */ AnyObject { - return this.serialize(effectState, new Set(['clear']), battle); - } - - deserializeEffectState(state: /* EffectState */ AnyObject, battle: Battle): EffectState { - const effectOrder: EffectState['effectOrder'] = state.effectOrder; - delete state.effectOrder; - const effectState = new EffectState(this.deserializeWithRefs(state, battle), battle, effectOrder); - return effectState; - } - serializeWithRefs(obj: unknown, battle: Battle): unknown { switch (typeof obj) { case 'function': @@ -329,7 +312,6 @@ export const State = new class { } if (this.isActiveMove(obj)) return this.serializeActiveMove(obj, battle); - if (this.isEffectState(obj)) return this.serializeEffectState(obj, battle); if (this.isReferable(obj)) return this.toRef(obj); if (obj.constructor !== Object) { // If we're getting this error, some 'special' field has been added to @@ -370,7 +352,6 @@ export const State = new class { } if (this.isActiveMove(obj)) return this.deserializeActiveMove(obj, battle); - if (this.isEffectState(obj)) return this.deserializeEffectState(obj, battle); const o: any = {}; for (const [key, value] of Object.entries(obj)) { From e38cd95d63b68a12580088ed0cbb1e8305f91b2b Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Mon, 20 Jan 2025 18:21:50 -0600 Subject: [PATCH 15/23] Improve Primal Orb immunity to negation --- sim/battle.ts | 2 -- sim/pokemon.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/sim/battle.ts b/sim/battle.ts index d9a900eb515d..6a068becfe39 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -24,7 +24,6 @@ import {State} from './state'; import {BattleQueue, Action} from './battle-queue'; import {BattleActions} from './battle-actions'; import {Utils} from '../lib/utils'; -import {ItemData} from './dex-items'; declare const __version: any; export type ChannelID = 0 | 1 | 2 | 3 | 4; @@ -567,7 +566,6 @@ export class Battle { return relayVar; } if (eventid !== 'Start' && eventid !== 'TakeItem' && effect.effectType === 'Item' && - !(eventid === 'SwitchIn' && (effect as ItemData).onSwitchInPriority === -1) && // <- questionable hack (target instanceof Pokemon) && target.ignoringItem()) { this.debug(eventid + ' handler suppressed by Embargo, Klutz or Magic Room'); return relayVar; diff --git a/sim/pokemon.ts b/sim/pokemon.ts index f97d9eaa725e..971a5ecd5448 100644 --- a/sim/pokemon.ts +++ b/sim/pokemon.ts @@ -839,7 +839,7 @@ export class Pokemon { } ignoringItem() { - return !!( + return !this.getItem().isPrimalOrb && !!( this.itemState.knockedOff || // Gen 3-4 (this.battle.gen >= 5 && !this.isActive) || (!this.getItem().ignoreKlutz && this.hasAbility('klutz')) || From 7b5183fa11d4f209ba6bdd32ac68e75c69610b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bastos=20Dias?= <80102738+andrebastosdias@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:41:54 +0000 Subject: [PATCH 16/23] Some simplifications (#73) * Remove unnused choices * Remove redundant isStarted * Override runSwitch * Complete comment * Reset abilityState.started * Update data/mods/gen7letsgo/scripts.ts --------- Co-authored-by: pyuk-bot --- data/abilities.ts | 2 +- data/items.ts | 2 +- data/mods/gen1/scripts.ts | 2 ++ data/mods/gen4/scripts.ts | 26 ++++++++++++++++++++ sim/battle-actions.ts | 51 +++++++++------------------------------ sim/battle-queue.ts | 4 +-- sim/battle.ts | 6 +---- sim/pokemon.ts | 1 + 8 files changed, 45 insertions(+), 49 deletions(-) diff --git a/data/abilities.ts b/data/abilities.ts index a347b6ff5f36..3d6c6337322c 100644 --- a/data/abilities.ts +++ b/data/abilities.ts @@ -607,7 +607,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { }, onUpdate(pokemon) { const ally = pokemon.allies()[0]; - if (this.gameType !== 'doubles' || !this.effectState.started && !pokemon.isStarted) return; + if (this.gameType !== 'doubles' || !this.effectState.started) return; if (pokemon.switchFlag || ally?.switchFlag) return; if (!ally || pokemon.baseSpecies.baseSpecies !== 'Tatsugiri' || ally.baseSpecies.baseSpecies !== 'Dondozo') { // Handle any edge cases diff --git a/data/items.ts b/data/items.ts index ff03ac8d89f7..79f78d38c5de 100644 --- a/data/items.ts +++ b/data/items.ts @@ -619,7 +619,7 @@ export const Items: import('../sim/dex-items').ItemDataTable = { ((this.effect as any).onUpdate as (p: Pokemon) => void).call(this, pokemon); }, onUpdate(pokemon) { - if (!this.effectState.started && !pokemon.isStarted || pokemon.transformed) return; + if (!this.effectState.started || pokemon.transformed) return; if (pokemon.hasAbility('protosynthesis') && !this.field.isWeather('sunnyday') && pokemon.useItem()) { pokemon.addVolatile('protosynthesis'); diff --git a/data/mods/gen1/scripts.ts b/data/mods/gen1/scripts.ts index 5617c4443093..a78a2bffc221 100644 --- a/data/mods/gen1/scripts.ts +++ b/data/mods/gen1/scripts.ts @@ -24,6 +24,7 @@ export const Scripts: ModdedBattleScriptsData = { }, // BattlePokemon scripts. pokemon: { + inherit: true, getStat(statName, unmodified) { // @ts-ignore - type checking prevents 'hp' from being passed, but we're paranoid if (statName === 'hp') throw new Error("Please read `maxhp` directly"); @@ -120,6 +121,7 @@ export const Scripts: ModdedBattleScriptsData = { }, }, actions: { + inherit: true, // This function is the main one when running a move. // It deals with the beforeMove event. // It also deals with how PP reduction works on gen 1. diff --git a/data/mods/gen4/scripts.ts b/data/mods/gen4/scripts.ts index a9279586fb30..995a600cfaaf 100644 --- a/data/mods/gen4/scripts.ts +++ b/data/mods/gen4/scripts.ts @@ -4,6 +4,32 @@ export const Scripts: ModdedBattleScriptsData = { actions: { inherit: true, + runSwitch(pokemon) { + this.battle.runEvent('EntryHazard', pokemon); + + this.battle.runEvent('SwitchIn', pokemon); + + if (this.battle.gen <= 2) { + // pokemon.lastMove is reset for all Pokemon on the field after a switch. This affects Mirror Move. + for (const poke of this.battle.getAllActive()) poke.lastMove = null; + if (!pokemon.side.faintedThisTurn && pokemon.draggedIn !== this.battle.turn) { + this.battle.runEvent('AfterSwitchInSelf', pokemon); + } + } + if (!pokemon.hp) return false; + pokemon.isStarted = true; + if (!pokemon.fainted) { + this.battle.singleEvent('Start', pokemon.getAbility(), pokemon.abilityState, pokemon); + this.battle.singleEvent('Start', pokemon.getItem(), pokemon.itemState, pokemon); + } + if (this.battle.gen === 4) { + for (const foeActive of pokemon.foes()) { + foeActive.removeVolatile('substitutebroken'); + } + } + pokemon.draggedIn = null; + return true; + }, modifyDamage(baseDamage, pokemon, target, move, suppressMessages = false) { // DPP divides modifiers into several mathematically important stages // The modifiers run earlier than other generations are called with ModifyDamagePhase1 and ModifyDamagePhase2 diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts index 72c481753635..aa074a626a23 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -169,48 +169,21 @@ export class BattleActions { return true; } runSwitch(pokemon: Pokemon) { - const battle = this.battle; - if (battle.gen >= 5) { - const switchersIn = [pokemon]; - for (let a = battle.queue.peek(); a?.choice === 'runSwitch'; a = battle.queue.peek()) { - const nextSwitch = battle.queue.shift(); - switchersIn.push(nextSwitch!.pokemon!); - } - const allActive = battle.getAllActive(true); - battle.speedSort(allActive); - battle.speedOrder = allActive.map((a) => a.side.n * battle.sides.length + a.position); - battle.fieldEvent('SwitchIn', switchersIn); - - for (const poke of switchersIn) { - if (!poke.hp) continue; - poke.isStarted = true; - poke.draggedIn = null; - } - return true; + const switchersIn = [pokemon]; + while (this.battle.queue.peek()?.choice === 'runSwitch') { + const nextSwitch = this.battle.queue.shift(); + switchersIn.push(nextSwitch!.pokemon!); } - battle.runEvent('EntryHazard', pokemon); - - battle.runEvent('SwitchIn', pokemon); + const allActive = this.battle.getAllActive(true); + this.battle.speedSort(allActive); + this.battle.speedOrder = allActive.map((a) => a.side.n * this.battle.sides.length + a.position); + this.battle.fieldEvent('SwitchIn', switchersIn); - if (battle.gen <= 2) { - // pokemon.lastMove is reset for all Pokemon on the field after a switch. This affects Mirror Move. - for (const poke of battle.getAllActive()) poke.lastMove = null; - if (!pokemon.side.faintedThisTurn && pokemon.draggedIn !== battle.turn) { - battle.runEvent('AfterSwitchInSelf', pokemon); - } - } - if (!pokemon.hp) return false; - pokemon.isStarted = true; - if (!pokemon.fainted) { - battle.singleEvent('Start', pokemon.getAbility(), pokemon.abilityState, pokemon); - battle.singleEvent('Start', pokemon.getItem(), pokemon.itemState, pokemon); - } - if (battle.gen === 4) { - for (const foeActive of pokemon.foes()) { - foeActive.removeVolatile('substitutebroken'); - } + for (const poke of switchersIn) { + if (!poke.hp) continue; + poke.isStarted = true; + poke.draggedIn = null; } - pokemon.draggedIn = null; return true; } diff --git a/sim/battle-queue.ts b/sim/battle-queue.ts index 9b2db39209dc..d0085fa76efb 100644 --- a/sim/battle-queue.ts +++ b/sim/battle-queue.ts @@ -92,7 +92,7 @@ export interface FieldAction { /** A generic action done by a single pokemon */ export interface PokemonAction { /** action type */ - choice: 'megaEvo' | 'megaEvoX' | 'megaEvoY' | 'shift' | 'runPrimal' | 'runSwitch' | 'event' | 'runUnnerve' | 'runDynamax' | 'terastallize'; + choice: 'megaEvo' | 'megaEvoX' | 'megaEvoY' | 'shift' | 'runSwitch' | 'event' | 'runDynamax' | 'terastallize'; /** priority of the action (lower first) */ priority: number; /** speed of pokemon doing action (higher first if priority tie) */ @@ -174,9 +174,7 @@ export class BattleQueue { beforeTurnMove: 5, revivalblessing: 6, - runUnnerve: 100, runSwitch: 101, - runPrimal: 102, switch: 103, megaEvo: 104, megaEvoX: 104, diff --git a/sim/battle.ts b/sim/battle.ts index 6a068becfe39..e53dc8354bbf 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -399,6 +399,7 @@ export class Battle { * 2. Priority, high to low (default 0) * 3. Speed, high to low (default 0) * 4. SubOrder, low to high (default 0) + * 5. EffectOrder, low to high (default 0) * * Doesn't reference `this` so doesn't need to be bound. */ @@ -2717,11 +2718,6 @@ export class Battle { case 'runSwitch': this.actions.runSwitch(action.pokemon); break; - case 'runPrimal': - if (!action.pokemon.transformed) { - this.singleEvent('Primal', action.pokemon.getItem(), action.pokemon.itemState, action.pokemon); - } - break; case 'shift': if (!action.pokemon.isActive) return false; if (action.pokemon.fainted) return false; diff --git a/sim/pokemon.ts b/sim/pokemon.ts index 971a5ecd5448..a2fabe00ce09 100644 --- a/sim/pokemon.ts +++ b/sim/pokemon.ts @@ -1488,6 +1488,7 @@ export class Pokemon { this.volatileStaleness = undefined; + this.abilityState.started = false; this.itemState.started = false; this.setSpecies(this.baseSpecies); From fb286657263d590bfe2d07f27e3b5213d0b9c076 Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Tue, 21 Jan 2025 22:51:12 -0600 Subject: [PATCH 17/23] Fix onSwitchInPriority for OMs and healreplacement Z-power --- config/formats.ts | 8 ++++---- data/conditions.ts | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/config/formats.ts b/config/formats.ts index 2035e2a38976..6be3b08a26a9 100644 --- a/config/formats.ts +++ b/config/formats.ts @@ -733,7 +733,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ if (!pokemon.m.abils.includes(effect)) pokemon.m.abils.push(effect); } }, - onSwitchInPriority: 2, + onSwitchInPriority: 100, onSwitchIn(pokemon) { let format = this.format; if (!format.getSharedPower) format = this.dex.formats.get('gen9sharedpower'); @@ -1568,7 +1568,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ } } }, - onSwitchInPriority: 2, + onSwitchInPriority: 100, onSwitchIn(pokemon) { if (pokemon.m.innates) { for (const innate of pokemon.m.innates) { @@ -1772,7 +1772,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ if (!pokemon.m.items.includes(effect)) pokemon.m.items.push(effect); } }, - onSwitchInPriority: 2, + onSwitchInPriority: 100, onSwitchIn(pokemon) { let format = this.format; if (!format.getSharedItems) format = this.dex.formats.get('gen9sharingiscaring'); @@ -2590,7 +2590,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ if (!pokemon.m.abils.includes(effect)) pokemon.m.abils.push(effect); } }, - onSwitchInPriority: 2, + onSwitchInPriority: 100, onSwitchIn(pokemon) { let format = this.format; if (!format.getSharedPower) format = this.dex.formats.get('gen9sharedpower'); diff --git a/data/conditions.ts b/data/conditions.ts index 0f4e73353ffa..ac7fb03f1945 100644 --- a/data/conditions.ts +++ b/data/conditions.ts @@ -423,7 +423,6 @@ export const Conditions: import('../sim/dex-conditions').ConditionDataTable = { this.effectState.sourceEffect = sourceEffect; this.add('-activate', source, 'healreplacement'); }, - onSwitchInPriority: 1, onSwitchIn(target) { if (!target.fainted) { target.heal(target.maxhp); From 6fac20648c27748a436081bba8d112b422da6b0f Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Tue, 21 Jan 2025 22:59:06 -0600 Subject: [PATCH 18/23] Give Klutz an onSwitchInPriority --- data/abilities.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/abilities.ts b/data/abilities.ts index 3d6c6337322c..df89745eb099 100644 --- a/data/abilities.ts +++ b/data/abilities.ts @@ -2216,6 +2216,9 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 51, }, klutz: { + // Klutz isn't technically active immediatlely in-game, but it activates early enough to beat all items + // we should keep an eye out in future gens for items that activate on switch-in before Unnerve + onSwitchInPriority: 1, // Item suppression implemented in Pokemon.ignoringItem() within sim/pokemon.js onStart(pokemon) { this.singleEvent('End', pokemon.getItem(), pokemon.itemState, pokemon); From 899f3258d013be2ace3fa28a94627c6c3343c809 Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Tue, 21 Jan 2025 23:34:21 -0600 Subject: [PATCH 19/23] Update Perish Body's subOrder --- sim/battle.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sim/battle.ts b/sim/battle.ts index e53dc8354bbf..0ff5543e3efb 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -926,7 +926,7 @@ export class Battle { Format: 5, Rule: 5, Ruleset: 5, - // Poison Touch: 6, + // Poison Touch: 6, (also includes Perish Body) Ability: 7, Item: 8, // Stall: 9, @@ -946,7 +946,7 @@ export class Battle { handler.subOrder = 5; } } else if (handler.effect.effectType === 'Ability') { - if (handler.effect.name === 'Poison Touch') { + if (handler.effect.name === 'Poison Touch' || handler.effect.name === 'Perish Body') { handler.subOrder = 6; } else if (handler.effect.name === 'Stall') { handler.subOrder = 9; From a3371bb5822d5932d168757a1ae6f64e0f31bf06 Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Tue, 21 Jan 2025 23:34:33 -0600 Subject: [PATCH 20/23] Update tests --- test/sim/abilities/costar.js | 13 +++++++++++++ test/sim/abilities/hospitality.js | 27 +++++++++++++++++++++++++++ test/sim/items/ejectpack.js | 2 +- test/sim/items/mirrorherb.js | 7 ++++--- test/sim/items/whiteherb.js | 1 - test/sim/misc/turn-order.js | 19 ++++++++++++++++++- 6 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 test/sim/abilities/hospitality.js diff --git a/test/sim/abilities/costar.js b/test/sim/abilities/costar.js index 42b53cb99a20..78fbf61f0fd4 100644 --- a/test/sim/abilities/costar.js +++ b/test/sim/abilities/costar.js @@ -57,5 +57,18 @@ describe('Costar', function () { assert.statStage(flamigo, 'def', -1, "A pokemon should copy the target's negative stat changes (def) when switching in with Costar."); assert.statStage(flamigo, 'spd', -1, "A pokemon should copy the target's negative stat changes (spd) when switching in with Costar."); }); + + it('should always activate later than Intimidate during simultaneous switch-ins', function () { + battle = common.createBattle({gameType: 'doubles'}, [[ + {species: 'flamigo', ability: 'costar', moves: ['sleeptalk']}, + {species: 'registeel', ability: 'clearbody', moves: ['sleeptalk']}, + ], [ + {species: 'litten', ability: 'intimidate', moves: ['sleeptalk']}, + {species: 'dipplin', ability: 'supersweetsyrup', moves: ['sleeptalk']}, + ]]); + const flamigo = battle.p1.active[0]; + assert.statStage(flamigo, 'atk', 0); + assert.statStage(flamigo, 'evasion', 0); + }); }); diff --git a/test/sim/abilities/hospitality.js b/test/sim/abilities/hospitality.js new file mode 100644 index 000000000000..9b1ee2a0951f --- /dev/null +++ b/test/sim/abilities/hospitality.js @@ -0,0 +1,27 @@ +'use strict'; + +const assert = require('./../../assert'); +const common = require('./../../common'); + +let battle; + +describe('Hospitality', function () { + afterEach(function () { + battle.destroy(); + }); + + it('should activate after hazards', function () { + battle = common.createBattle({gameType: 'doubles'}, [[ + {species: 'snom', level: 1, ability: 'noguard', moves: ['sleeptalk']}, + {species: 'snom', level: 1, ability: 'noguard', moves: ['sleeptalk']}, + {species: 'deerling', moves: ['sleeptalk']}, + {species: 'sinistcha', ability: 'hospitality', moves: ['sleeptalk']}, + ], [ + {species: 'kleavor', moves: ['stoneaxe']}, + {species: 'kleavor', moves: ['stoneaxe']}, + ]]); + battle.makeChoices(); + battle.makeChoices('switch 3, switch 4'); + assert.fullHP(battle.p1.pokemon[0].status); + }); +}); diff --git a/test/sim/items/ejectpack.js b/test/sim/items/ejectpack.js index c01f1804b6b6..704c396908e0 100644 --- a/test/sim/items/ejectpack.js +++ b/test/sim/items/ejectpack.js @@ -128,7 +128,7 @@ describe(`Eject Pack`, function () { assert.species(battle.p2.active[1], 'Wynaut'); }); - it(`should not trigger until after all entrance abilities have resolved during simultaneous switches`, function () { + it(`should not prevent entrance Abilities from resolving during simultaneous switches`, function () { battle = common.createBattle({gameType: 'doubles'}, [[ {species: 'Hydreigon', ability: 'intimidate', moves: ['sleeptalk']}, {species: 'Wynaut', moves: ['sleeptalk']}, diff --git a/test/sim/items/mirrorherb.js b/test/sim/items/mirrorherb.js index f75158be8b76..eb05ea886ea2 100644 --- a/test/sim/items/mirrorherb.js +++ b/test/sim/items/mirrorherb.js @@ -39,11 +39,12 @@ describe("Mirror Herb", () => { {species: 'Primeape', ability: 'Defiant', item: 'Weakness Policy', moves: ['sleeptalk', 'haze']}, {species: 'Annihilape', ability: 'Defiant', item: 'Weakness Policy', moves: ['sleeptalk', 'howl']}, ]]); - assert.statStage(battle.p1.active[0], 'atk', 4, `Mirror Herb should have copied both Defiant boosts but only boosted atk by ${battle.p1.active[0].boosts.atk}`); + const electrode = battle.p1.active[0]; + assert.statStage(electrode, 'atk', 4, `Mirror Herb should have copied both Defiant boosts but only boosted atk by ${electrode.boosts.atk}`); battle.makeChoices('auto', 'move haze, move howl'); - assert.statStage(battle.p1.active[0], 'atk', 2, `Mirror Herb should have copied both Howl boosts but only boosted atk by ${battle.p1.active[0].boosts.atk}`); + assert.statStage(electrode, 'atk', 2, `Mirror Herb should have copied both Howl boosts but only boosted atk by ${electrode.boosts.atk}`); battle.makeChoices('move recycle, move air cutter', 'auto'); - assert.statStage(battle.p1.active[0], 'spa', 4, `Mirror Herb should have copied all Weakness Policy boosts but only boosted spa by ${battle.p1.active[0].boosts.spa}`); + assert.statStage(electrode, 'spa', 4, `Mirror Herb should have copied all Weakness Policy boosts but only boosted spa by ${electrode.boosts.spa}`); }); it("should wait for most entrance abilities before copying all their (opposing) boosts", () => { diff --git a/test/sim/items/whiteherb.js b/test/sim/items/whiteherb.js index 2806edc1655e..73229dbe4935 100644 --- a/test/sim/items/whiteherb.js +++ b/test/sim/items/whiteherb.js @@ -76,6 +76,5 @@ describe("White Herb", function () { const flittle = battle.p2.active[0]; assert.false.holdsItem(flittle); assert.statStage(flittle, 'atk', 1); - common.saveReplay(battle); }); }); diff --git a/test/sim/misc/turn-order.js b/test/sim/misc/turn-order.js index 3c2bf04a423d..06a6ec76370b 100644 --- a/test/sim/misc/turn-order.js +++ b/test/sim/misc/turn-order.js @@ -154,7 +154,7 @@ describe('Pokemon Speed', function () { }); }); -describe('Switching', function () { +describe('Switching out', function () { it('should happen in order of switch-out\'s Speed stat', function () { battle = common.createBattle(); const p1team = [ @@ -173,6 +173,23 @@ describe('Switching', function () { }); }); +describe('Switching in', function () { + it(`should trigger events in an order determined by what each Pokemon's speed was when they switched in`, function () { + battle = common.gen(7).createBattle([[ + {species: "ribombee", moves: ['stickyweb']}, + {species: "groudon", item: 'redorb', moves: ['sleeptalk'], evs: {spe: 0}}, + ], [ + {species: "golemalola", ability: 'galvanize', moves: ['explosion']}, + {species: "kyogre", item: 'blueorb', moves: ['sleeptalk'], evs: {spe: 252}}, + ]]); + battle.makeChoices(); + battle.makeChoices('switch 2', 'switch 2'); + const kyogre = battle.p2.active[0]; + assert.statStage(kyogre, 'spe', -1); + assert.equal(battle.field.weather, 'desolateland', 'Groudon should have reverted after Kyogre in spite of Sticky Web because it was slower before the SwitchIn event started'); + }); +}); + describe('Speed ties', function () { it('(slow) Perish Song faint order should be random', function () { const wins = {p1: 0, p2: 0}; From 84578f980721c6a903dc56a7f3a620a9f9426392 Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Tue, 21 Jan 2025 23:49:24 -0600 Subject: [PATCH 21/23] Don't activate Commander early if only Dondozo is switching in --- data/abilities.ts | 7 ++++--- sim/pokemon.ts | 4 ++-- test/sim/abilities/commander.js | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/data/abilities.ts b/data/abilities.ts index df89745eb099..76c7c1d51000 100644 --- a/data/abilities.ts +++ b/data/abilities.ts @@ -598,16 +598,17 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { commander: { onAnySwitchInPriority: -2, onAnySwitchIn() { - this.effectState.started = true; ((this.effect as any).onUpdate as (p: Pokemon) => void).call(this, this.effectState.target); }, onStart(pokemon) { - this.effectState.started = true; ((this.effect as any).onUpdate as (p: Pokemon) => void).call(this, pokemon); }, onUpdate(pokemon) { + if (this.gameType !== 'doubles') return; + // don't run between when a Pokemon switches in and the resulting onSwitchIn event + if (this.queue.peek()?.choice === 'runSwitch') return; + const ally = pokemon.allies()[0]; - if (this.gameType !== 'doubles' || !this.effectState.started) return; if (pokemon.switchFlag || ally?.switchFlag) return; if (!ally || pokemon.baseSpecies.baseSpecies !== 'Tatsugiri' || ally.baseSpecies.baseSpecies !== 'Dondozo') { // Handle any edge cases diff --git a/sim/pokemon.ts b/sim/pokemon.ts index a2fabe00ce09..5776cdc3b310 100644 --- a/sim/pokemon.ts +++ b/sim/pokemon.ts @@ -1488,8 +1488,8 @@ export class Pokemon { this.volatileStaleness = undefined; - this.abilityState.started = false; - this.itemState.started = false; + delete this.abilityState.started; + delete this.itemState.started; this.setSpecies(this.baseSpecies); } diff --git a/test/sim/abilities/commander.js b/test/sim/abilities/commander.js index 868966856492..d8d18fc51190 100644 --- a/test/sim/abilities/commander.js +++ b/test/sim/abilities/commander.js @@ -269,6 +269,6 @@ describe('Commander', function () { battle.makeChoices(); const tatsugiri = battle.p2.pokemon[0]; - assert(tatsugiri.status, 'psn'); + assert.equal(tatsugiri.status, 'psn'); }); }); From d79da78a82f1624647592f8d8fae0fcc830f9a3f Mon Sep 17 00:00:00 2001 From: pyuk-bot Date: Wed, 22 Jan 2025 17:48:30 -0600 Subject: [PATCH 22/23] Fix Klutz comment typo --- data/abilities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/abilities.ts b/data/abilities.ts index 76c7c1d51000..f38453c64a8e 100644 --- a/data/abilities.ts +++ b/data/abilities.ts @@ -2217,7 +2217,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 51, }, klutz: { - // Klutz isn't technically active immediatlely in-game, but it activates early enough to beat all items + // Klutz isn't technically active immediately in-game, but it activates early enough to beat all items // we should keep an eye out in future gens for items that activate on switch-in before Unnerve onSwitchInPriority: 1, // Item suppression implemented in Pokemon.ignoringItem() within sim/pokemon.js From ce0a96601a01a6a9dd98a1b216cdf594684f2ec2 Mon Sep 17 00:00:00 2001 From: MacChaeger Date: Wed, 22 Jan 2025 18:42:44 -0600 Subject: [PATCH 23/23] Very minor changes --- sim/battle-actions.ts | 2 +- sim/battle.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts index aa074a626a23..41fd9852c88d 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -176,7 +176,7 @@ export class BattleActions { } const allActive = this.battle.getAllActive(true); this.battle.speedSort(allActive); - this.battle.speedOrder = allActive.map((a) => a.side.n * this.battle.sides.length + a.position); + this.battle.speedOrder = allActive.map((a) => a.side.n * a.battle.sides.length + a.position); this.battle.fieldEvent('SwitchIn', switchersIn); for (const poke of switchersIn) { diff --git a/sim/battle.ts b/sim/battle.ts index 0ff5543e3efb..a2837667d807 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -965,7 +965,7 @@ export class Battle { handler.speed = pokemon.speed; if (callbackName.endsWith('SwitchIn')) { // Pokemon speeds including ties are resolved before all onSwitchIn handlers and aren't re-sorted in-between - // so we subtract a fractional speed to each Pokemon's respective event handlers by using the index of their + // so we subtract a fractional speed from each Pokemon's respective event handlers by using the index of their // unique field position in a pre-sorted-by-speed array const fieldPositionValue = pokemon.side.n * this.sides.length + pokemon.position; handler.speed -= this.speedOrder.indexOf(fieldPositionValue) / (this.activePerHalf * 2);