diff --git a/config/formats.ts b/config/formats.ts index b3ce64fd1bc1..2035e2a38976 100644 --- a/config/formats.ts +++ b/config/formats.ts @@ -638,7 +638,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 +646,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 +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] = {id: this.toID(effect), target: pokemon}; + 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); } @@ -1462,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] = {id: pokemon.m.innate, target: pokemon}; + 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] = {id: ally.m.innate, target: ally}; + ally.volatiles[ally.m.innate] = this.initEffectState({id: ally.m.innate, target: ally}); ally.m.startVolatile = true; } } @@ -1767,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] = {id: this.toID(effect), target: pokemon}; + 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); } @@ -2585,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] = {id: this.toID(effect), target: pokemon}; + 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/abilities.ts b/data/abilities.ts index 46747db16e53..a347b6ff5f36 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.effect as any).onStart as (p: Pokemon) => void).call(this, 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.effect as any).onStart as (p: Pokemon) => void).call(this, 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,9 +596,19 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 213, }, 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; const ally = pokemon.allies()[0]; + 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'); @@ -693,6 +689,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 +1052,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 +1062,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 +1320,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 +1408,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 +1812,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 +1907,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 +2055,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 +2502,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 +2830,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 +2967,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", @@ -3107,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; @@ -3420,6 +3427,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 +3564,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 +3960,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 +4155,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 +4243,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 +4883,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 +5045,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 +5186,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 +5572,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 99d707c02b1b..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 = {id: '', target}; + 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 = {id: '', target}; + this.clearEffectState(target.itemState); 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.effect as any).onUpdate as (p: Pokemon) => void).call(this, pokemon); }, onUpdate(pokemon) { - if (!this.effectState.started || pokemon.transformed) return; - if (this.queue.peek(true)?.choice === 'runSwitch') return; + if (!this.effectState.started && !pokemon.isStarted || pokemon.transformed) 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(); @@ -3802,19 +3804,36 @@ export const Items: import('../sim/dex-items').ItemDataTable = { }, onFoeAfterBoost(boost, target, source, effect) { 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; - pokemon.useItem(); - this.boost(boostPlus, 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; + (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() { + delete this.effectState.boosts; + delete this.effectState.ready; }, num: 1883, gen: 9, @@ -3825,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(); @@ -4522,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(); @@ -4747,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", @@ -4893,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(); @@ -7170,6 +7191,13 @@ export const Items: import('../sim/dex-items').ItemDataTable = { } }, }, + 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); + }, 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..06628842f26f 100644 --- a/data/mods/partnersincrime/moves.ts +++ b/data/mods/partnersincrime/moves.ts @@ -49,6 +49,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 +151,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { } source.ability = targetAbility.id; - source.abilityState = {id: this.toID(source.ability), target: source}; + 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; @@ -163,7 +166,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { } target.ability = sourceAbility.id; - target.abilityState = {id: this.toID(target.ability), target: target}; + 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 68f391110d89..9088cddc9a0a 100644 --- a/data/mods/partnersincrime/scripts.ts +++ b/data/mods/partnersincrime/scripts.ts @@ -218,53 +218,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 +239,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 = 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) { @@ -305,7 +258,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..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 = {id: '', target}; + 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 = {id: '', target}; + 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 5222a9bc84c2..38a941f5d73b 100644 --- a/data/mods/sharingiscaring/scripts.ts +++ b/data/mods/sharingiscaring/scripts.ts @@ -1,4 +1,4 @@ -import {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 = {id: '', target: this}; + 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 = {id: '', target: this}; + 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 = {id: item.id, target: this}; + 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 f850aa8b5ecc..f90485ffef45 100644 --- a/data/moves.ts +++ b/data/moves.ts @@ -3768,7 +3768,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 +4909,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 +7483,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 +8637,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 +10948,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 +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 = {id: this.toID(source.ability), target: source}; - target.abilityState = {id: this.toID(target.ability), target: target}; + 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); @@ -18163,7 +18169,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 +18490,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 +18618,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 +19704,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 +20515,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 +21731,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 22c2873ed52c..72c481753635 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); @@ -134,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}); } @@ -169,32 +169,43 @@ export class BattleActions { return true; } runSwitch(pokemon: Pokemon) { - this.battle.runEvent('Swap', 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); - if (this.battle.gen >= 5) { - this.battle.runEvent('SwitchIn', pokemon); + for (const poke of switchersIn) { + if (!poke.hp) continue; + poke.isStarted = true; + poke.draggedIn = null; + } + return true; } + battle.runEvent('EntryHazard', pokemon); - this.battle.runEvent('EntryHazard', pokemon); - - if (this.battle.gen <= 4) { - 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'); } @@ -328,6 +339,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 = []; @@ -342,7 +354,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 a2d276ef5c30..d9a900eb515d 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; @@ -93,6 +94,7 @@ interface EventListener extends EventListenerWithoutPriority { order: number | false; priority: number; subOrder: number; + effectOrder?: number; speed?: number; } @@ -171,8 +173,9 @@ 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; + speedOrder: number[]; teamGenerator: ReturnType | null; @@ -214,7 +217,7 @@ export class Battle { options.forceRandomChance : null; this.deserialized = !!options.deserialized; this.strictChoices = !!options.strictChoices; - this.formatData = {id: format.id}; + 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; @@ -244,7 +247,7 @@ export class Battle { this.ended = false; this.effect = {id: ''} as Effect; - this.effectState = {id: ''}; + this.effectState = this.initEffectState({id: ''}); this.event = {id: ''}; this.events = null; @@ -258,8 +261,12 @@ export class Battle { this.lastMoveLine = -1; this.lastSuccessfulMoveThisTurn = null; this.lastDamage = 0; - this.abilityOrder = 0; + this.effectOrder = 0; this.quickClawRoll = false; + this.speedOrder = []; + for (let i = 0; i < this.activePerHalf * 2; i++) { + this.speedOrder.push(i); + } this.teamGenerator = null; @@ -401,13 +408,15 @@ export class Battle { ((b.priority || 0) - (a.priority || 0)) || ((b.speed || 0) - (a.speed || 0)) || -((b.subOrder || 0) - (a.subOrder || 0)) || + -((b.effectOrder || 0) - (a.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 +480,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 +512,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 +526,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 +536,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 +566,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 +592,7 @@ export class Battle { const parentEvent = this.event; this.effect = effect; - this.effectState = state || {}; + this.effectState = state as EffectState || this.initEffectState({}); this.event = {id: eventid, target, source, effect: sourceEffect}; this.eventDepth++; @@ -726,7 +745,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: this.initEffectState({}), end: null, effectHolder: target, }, `on${eventid}`)); } } @@ -839,7 +858,7 @@ export class Battle { const parentEffect = this.effect; const parentEffectState = this.effectState; this.effect = handler.effect; - this.effectState = handler.state || {}; + this.effectState = handler.state || this.initEffectState({}); this.effectState.target = effectHolder; returnVal = handler.callback.apply(this, args); @@ -888,17 +907,72 @@ 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 (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) { - (handler as EventListener).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 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 as EventListener; + return handler; } findEventHandlers(target: Pokemon | Pokemon[] | Side | Battle, eventName: string, source?: Pokemon | null) { @@ -1009,7 +1083,7 @@ export class Battle { state: slotConditionState, end: side.removeSlotCondition, endCallArgs: [side, pokemon, slotCondition.id], - effectHolder: side, + effectHolder: pokemon, }, callbackName)); } } @@ -1017,7 +1091,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 +1100,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, }); } } @@ -1182,11 +1256,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); } } @@ -2372,6 +2446,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; @@ -2641,9 +2716,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; @@ -2666,7 +2738,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; } @@ -2710,7 +2782,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); @@ -3153,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/dex-abilities.ts b/sim/dex-abilities.ts index 5d60ee825d29..8f5f03feb1d6 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.onAnySwitchIn) { + 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..f8c160c98046 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; @@ -553,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 @@ -616,6 +624,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..f0bbf26ae466 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.onAnySwitchIn) { + 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..58862730ad3a 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 = this.battle.initEffectState({id: ''}); this.terrain = ''; - this.terrainState = {id: ''}; + 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 = {id: status.id}; + 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 = {id: ''}; + 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 = { + this.terrainState = this.battle.initEffectState({ id: status.id, source, sourceSlot: source.getSlot(), duration: status.duration, - }; + }); 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.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] = { + state = this.pseudoWeather[status.id] = this.battle.initEffectState({ id: status.id, source, sourceSlot: source?.getSlot(), duration: status.duration, - }; + }); 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 dbffa6995071..f97d9eaa725e 100644 --- a/sim/pokemon.ts +++ b/sim/pokemon.ts @@ -31,8 +31,9 @@ interface Attacker { } export interface EffectState { - // TODO: set this to be an actual number after converting data/ to .ts - duration?: number | any; + id: string; + effectOrder: number; + duration?: number; [k: string]: any; } @@ -250,7 +251,6 @@ export class Pokemon { weighthg: number; speed: number; - abilityOrder: number; canMegaEvo: string | null | undefined; canMegaEvoX: string | null | undefined; @@ -309,7 +309,7 @@ export class Pokemon { if (set.name === set.species || !set.name) { set.name = this.baseSpecies.baseSpecies; } - this.speciesState = {id: this.species.id}; + this.speciesState = this.battle.initEffectState({id: this.species.id}); this.name = set.name.substr(0, 20); this.fullname = this.side.id + ': ' + this.name; @@ -357,7 +357,7 @@ export class Pokemon { (this.gender === '' ? '' : ', ' + this.gender) + (this.set.shiny ? ', shiny' : ''); this.status = ''; - this.statusState = {}; + this.statusState = this.battle.initEffectState({}); this.volatiles = {}; this.showCure = undefined; @@ -400,10 +400,10 @@ export class Pokemon { this.baseAbility = toID(set.ability); this.ability = this.baseAbility; - this.abilityState = {id: this.ability}; + this.abilityState = this.battle.initEffectState({id: this.ability, target: this}); this.item = toID(set.item); - this.itemState = {id: this.item}; + this.itemState = this.battle.initEffectState({id: this.item, target: this}); this.lastItem = ''; this.usedItemThisTurn = false; this.ateBerry = false; @@ -461,12 +461,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 +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] = {...pokemon.volatiles[i]}; + this.volatiles[i] = this.battle.initEffectState({...pokemon.volatiles[i]}); if (this.volatiles[i].linkedPokemon) { delete pokemon.volatiles[i].linkedPokemon; delete pokemon.volatiles[i].linkedStatus; @@ -1400,7 +1394,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); @@ -1494,6 +1488,8 @@ export class Pokemon { this.volatileStaleness = undefined; + this.itemState.started = false; + this.setSpecies(this.baseSpecies); } @@ -1665,7 +1661,7 @@ export class Pokemon { } this.status = status.id; - this.statusState = {id: status.id, target: this}; + 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) { @@ -1731,7 +1727,7 @@ export class Pokemon { this.lastItem = this.item; this.item = ''; - this.itemState = {id: '', target: this}; + this.battle.clearEffectState(this.itemState); this.usedItemThisTurn = true; this.ateBerry = true; this.battle.runEvent('AfterUseItem', this, null, null, item); @@ -1768,7 +1764,7 @@ export class Pokemon { this.lastItem = this.item; this.item = ''; - this.itemState = {id: '', target: this}; + this.battle.clearEffectState(this.itemState); this.usedItemThisTurn = true; this.battle.runEvent('AfterUseItem', this, null, null, item); return true; @@ -1788,7 +1784,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.battle.clearEffectState(this.itemState); this.pendingStaleness = undefined; this.battle.singleEvent('End', item, oldItemState, this); this.battle.runEvent('AfterTakeItem', this, null, null, item); @@ -1813,7 +1809,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 = 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); @@ -1855,12 +1851,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 = 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); } - this.abilityOrder = this.battle.abilityOrder++; return oldAbility; } @@ -1915,7 +1910,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] = 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 a4707d65c967..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] = { + this.sideConditions[status.id] = this.battle.initEffectState({ id: status.id, target: this, source, sourceSlot: source.getSlot(), duration: status.duration, - }; + }); 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] = this.battle.initEffectState({ id: status.id, target: this, source, sourceSlot: source.getSlot(), + isSlotCondition: true, duration: status.duration, - }; + }); if (status.durationCallback) { conditionState.duration = status.durationCallback.call(this.battle, this.active[0], source, sourceEffect); diff --git a/test/sim/abilities/commander.js b/test/sim/abilities/commander.js index 9cf3b72af474..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.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); 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']}, @@ -241,7 +255,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/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'); 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/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'); diff --git a/test/sim/items/mirrorherb.js b/test/sim/items/mirrorherb.js new file mode 100644 index 000000000000..f75158be8b76 --- /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']}, + ]]); + assert.statStage(battle.p1.active[0], 'atk', -1); + 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, `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, `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, `Mirror Herb should have copied all Weakness Policy boosts but only boosted spa by ${battle.p1.active[0].boosts.spa}`); + }); + + 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']}, + ], [ + {species: 'Zacian', ability: 'Intrepid Sword', moves: ['sleeptalk']}, + {species: 'Annihilape', ability: 'Defiant', moves: ['sleeptalk']}, + ]]); + 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..2806edc1655e 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']}, @@ -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); + }); }); 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/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/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'}, 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');