diff --git a/data/abilities.ts b/data/abilities.ts index 2bbe4b3de8..a9080bd0c1 100644 --- a/data/abilities.ts +++ b/data/abilities.ts @@ -373,18 +373,20 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { }, beadsofruin: { onStart(pokemon) { - if (this.suppressingAbility(pokemon)) return; this.add('-ability', pokemon, 'Beads of Ruin'); + this.field.addSourcedPseudoWeather('beadsofruin', pokemon, this.effect); }, - onAnyModifySpD(spd, target, source, move) { - const abilityHolder = this.effectState.target; - if (target.hasAbility('Beads of Ruin')) return; - if (!move.ruinedSpD?.hasAbility('Beads of Ruin')) move.ruinedSpD = abilityHolder; - if (move.ruinedSpD !== abilityHolder) return; - this.debug('Beads of Ruin SpD drop'); - return this.chainModify(0.75); + onEnd(pokemon) { + this.field.removePseudoWeatherSource('beadsofruin', pokemon); }, - flags: {}, + condition: { + onModifySpD(spd, target, source, move) { + if (this.field.pseudoWeather['beadsofruin'].activeSources.includes(target)) return; + this.debug('Beads of Ruin SpD drop'); + return this.chainModify(0.75); + }, + }, + flags: { breakable: 1 }, name: "Beads of Ruin", rating: 4.5, num: 284, @@ -2862,60 +2864,56 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { onSwitchInPriority: 2, onSwitchIn(pokemon) { this.add('-ability', pokemon, 'Neutralizing Gas'); - pokemon.abilityState.ending = false; - const strongWeathers = ['desolateland', 'primordialsea', 'deltastream']; - for (const target of this.getAllActive()) { - if (target.hasItem('Ability Shield')) { - this.add('-block', target, 'item: Ability Shield'); - continue; - } - // Can't suppress a Tatsugiri inside of Dondozo already - if (target.volatiles['commanding']) { - continue; - } - if (target.illusion) { - this.singleEvent('End', this.dex.abilities.get('Illusion'), target.abilityState, target, pokemon, 'neutralizinggas'); - } - if (target.volatiles['slowstart']) { - delete target.volatiles['slowstart']; - this.add('-end', target, 'Slow Start', '[silent]'); - } - if (strongWeathers.includes(target.getAbility().id)) { - this.singleEvent('End', this.dex.abilities.get(target.getAbility().id), target.abilityState, target, pokemon, 'neutralizinggas'); - } - } + this.field.addSourcedPseudoWeather('neutralizinggas', pokemon, this.effect); }, - onEnd(source) { - if (source.transformed) return; - for (const pokemon of this.getAllActive()) { - if (pokemon !== source && pokemon.hasAbility('Neutralizing Gas')) { - return; - } + onEnd(pokemon) { + if (!this.getAllActive().some(active => active !== pokemon && active.hasAbility('neutralizinggas'))) { + this.add('-end', pokemon, 'ability: Neutralizing Gas'); } - this.add('-end', source, 'ability: Neutralizing Gas'); // FIXME this happens before the pokemon switches out, should be the opposite order. // Not an easy fix since we cant use a supported event. Would need some kind of special event that // gathers events to run after the switch and then runs them when the ability is no longer accessible. // (If you're tackling this, do note extreme weathers have the same issue) - // Mark this pokemon's ability as ending so Pokemon#ignoringAbility skips it - if (source.abilityState.ending) return; - source.abilityState.ending = true; - const sortedActive = this.getAllActive(); - this.speedSort(sortedActive); - for (const pokemon of sortedActive) { - if (pokemon !== source) { + this.field.removePseudoWeatherSource('neutralizinggas', pokemon); + }, + condition: { + onFieldStart() { + const strongWeathers = ['desolateland', 'primordialsea', 'deltastream']; + for (const target of this.getAllActive()) { + if (target.hasItem('Ability Shield')) { + this.add('-block', target, 'item: Ability Shield'); + continue; + } + // Can't suppress a Tatsugiri inside of Dondozo already + if (target.volatiles['commanding']) continue; + if (target.illusion) { + this.singleEvent('End', this.dex.abilities.get('Illusion'), target.abilityState, target, null, 'neutralizinggas'); + } + if (target.volatiles['slowstart']) { + delete target.volatiles['slowstart']; + this.add('-end', target, 'Slow Start', '[silent]'); + } + if (strongWeathers.includes(target.getAbility().id)) { + this.singleEvent('End', this.dex.abilities.get(target.getAbility().id), target.abilityState, target, null, 'neutralizinggas'); + } + } + }, + onFieldEnd() { + const sortedActive = this.getAllActive(); + this.speedSort(sortedActive); + for (const pokemon of sortedActive) { if (pokemon.getAbility().flags['cantsuppress']) continue; // does not interact with e.g Ice Face, Zen Mode if (pokemon.hasItem('abilityshield')) continue; // don't restart abilities that weren't suppressed // Will be suppressed by Pokemon#ignoringAbility if needed this.singleEvent('Start', pokemon.getAbility(), pokemon.abilityState, pokemon); - if (pokemon.ability === "gluttony") { + if (pokemon.ability === 'gluttony') { pokemon.abilityState.gluttony = false; } } - } + }, }, flags: { failroleplay: 1, noreceiver: 1, noentrain: 1, notrace: 1, failskillswap: 1, notransform: 1 }, name: "Neutralizing Gas", @@ -4775,18 +4773,20 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { }, swordofruin: { onStart(pokemon) { - if (this.suppressingAbility(pokemon)) return; this.add('-ability', pokemon, 'Sword of Ruin'); + this.field.addSourcedPseudoWeather('swordofruin', pokemon, this.effect); }, - onAnyModifyDef(def, target, source, move) { - const abilityHolder = this.effectState.target; - if (target.hasAbility('Sword of Ruin')) return; - if (!move.ruinedDef?.hasAbility('Sword of Ruin')) move.ruinedDef = abilityHolder; - if (move.ruinedDef !== abilityHolder) return; - this.debug('Sword of Ruin Def drop'); - return this.chainModify(0.75); + onEnd(pokemon) { + this.field.removePseudoWeatherSource('swordofruin', pokemon); }, - flags: {}, + condition: { + onModifyDef(def, target, source, move) { + if (this.field.pseudoWeather['swordofruin'].activeSources.includes(target)) return; + this.debug('Sword of Ruin Def drop'); + return this.chainModify(0.75); + }, + }, + flags: { breakable: 1 }, name: "Sword of Ruin", rating: 4.5, num: 285, @@ -4828,18 +4828,20 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { }, tabletsofruin: { onStart(pokemon) { - if (this.suppressingAbility(pokemon)) return; this.add('-ability', pokemon, 'Tablets of Ruin'); + this.field.addSourcedPseudoWeather('tabletsofruin', pokemon, this.effect); }, - onAnyModifyAtk(atk, source, target, move) { - const abilityHolder = this.effectState.target; - if (source.hasAbility('Tablets of Ruin')) return; - if (!move.ruinedAtk) move.ruinedAtk = abilityHolder; - if (move.ruinedAtk !== abilityHolder) return; - this.debug('Tablets of Ruin Atk drop'); - return this.chainModify(0.75); + onEnd(pokemon) { + this.field.removePseudoWeatherSource('tabletsofruin', pokemon); }, - flags: {}, + condition: { + onModifyAtk(atk, target, source, move) { + if (this.field.pseudoWeather['tabletsofruin'].activeSources.includes(target)) return; + this.debug('Tablets of Ruin Atk drop'); + return this.chainModify(0.75); + }, + }, + flags: { breakable: 1 }, name: "Tablets of Ruin", rating: 4.5, num: 284, @@ -5241,18 +5243,20 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { }, vesselofruin: { onStart(pokemon) { - if (this.suppressingAbility(pokemon)) return; this.add('-ability', pokemon, 'Vessel of Ruin'); + this.field.addSourcedPseudoWeather('vesselofruin', pokemon, this.effect); }, - onAnyModifySpA(spa, source, target, move) { - const abilityHolder = this.effectState.target; - if (source.hasAbility('Vessel of Ruin')) return; - if (!move.ruinedSpA) move.ruinedSpA = abilityHolder; - if (move.ruinedSpA !== abilityHolder) return; - this.debug('Vessel of Ruin SpA drop'); - return this.chainModify(0.75); + onEnd(pokemon) { + this.field.removePseudoWeatherSource('vesselofruin', pokemon); }, - flags: {}, + condition: { + onModifySpA(spa, target, source, move) { + if (this.field.pseudoWeather['vesselofruin'].activeSources.includes(target)) return; + this.debug('Vessel of Ruin SpA drop'); + return this.chainModify(0.75); + }, + }, + flags: { breakable: 1 }, name: "Vessel of Ruin", rating: 4.5, num: 284, diff --git a/data/mods/gen9ssb/abilities.ts b/data/mods/gen9ssb/abilities.ts index 10afe20079..245302a4c2 100644 --- a/data/mods/gen9ssb/abilities.ts +++ b/data/mods/gen9ssb/abilities.ts @@ -230,22 +230,23 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa desc: "Active Pokemon without this Ability have their Defense multiplied by 0.85x. This Pokemon's moves and their effects ignore certain Abilities of other Pokemon.", name: "Quag of Ruin", onStart(pokemon) { - if (this.suppressingAbility(pokemon)) return; this.add('-ability', pokemon, 'Quag of Ruin'); + this.field.addSourcedPseudoWeather('quagofruin', pokemon, this.effect); }, - onAnyModifyDef(def, target, source, move) { - if (!move) return; - const abilityHolder = this.effectState.target; - if (target.hasAbility('Quag of Ruin')) return; - if (!move.ruinedDef?.hasAbility('Quag of Ruin')) move.ruinedDef = abilityHolder; - if (move.ruinedDef !== abilityHolder) return; - this.debug('Quag of Ruin Def drop'); - return this.chainModify(0.85); + onEnd(pokemon) { + this.field.removePseudoWeatherSource('quagofruin', pokemon); + }, + condition: { + onModifyDef(def, target, source, move) { + if (this.field.pseudoWeather['quagofruin'].activeSources.includes(target)) return; + this.debug('Quag of Ruin Def drop'); + return this.chainModify(0.85); + }, }, onModifyMove(move) { move.ignoreAbility = true; }, - flags: {}, + flags: { breakable: 1 }, gen: 9, }, clodofruin: { @@ -254,15 +255,17 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa name: "Clod of Ruin", onStart(pokemon) { this.add('-ability', pokemon, 'Clod of Ruin'); + this.field.addSourcedPseudoWeather('clodofruin', pokemon, this.effect); }, - onAnyModifyAtk(atk, target, source, move) { - if (!move) return; - const abilityHolder = this.effectState.target; - if (target.hasAbility('Clod of Ruin')) return; - if (!move.ruinedAtk?.hasAbility('Clod of Ruin')) move.ruinedAtk = abilityHolder; - if (move.ruinedAtk !== abilityHolder) return; - this.debug('Clod of Ruin Atk drop'); - return this.chainModify(0.85); + onEnd(pokemon) { + this.field.removePseudoWeatherSource('clodofruin', pokemon); + }, + condition: { + onModifyAtk(atk, target, source, move) { + if (this.field.pseudoWeather['clodofruin'].activeSources.includes(target)) return; + this.debug('Clod of Ruin Atk drop'); + return this.chainModify(0.85); + }, }, onAnyModifyBoost(boosts, pokemon) { const unawareUser = this.effectState.target; @@ -474,11 +477,17 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa onStart(pokemon) { this.add('-ability', pokemon, 'Blitz of Ruin'); this.add('-message', `${pokemon.name}'s Blitz of Ruin lowered the Speed of all surrounding Pokémon!`); + this.field.addSourcedPseudoWeather('blitzofruin', pokemon, this.effect); }, - onAnyModifySpe(spe, pokemon) { - if (!pokemon.hasAbility('Blitz of Ruin')) { + onEnd(pokemon) { + this.field.removePseudoWeatherSource('blitzofruin', pokemon); + }, + condition: { + onModifySpe(spe, target) { + if (this.field.pseudoWeather['blitzofruin'].activeSources.includes(target)) return; + this.debug('Blitz of Ruin Spe drop'); return this.chainModify(0.75); - } + }, }, flags: { breakable: 1 }, }, diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts index 66fd617e6d..d76ae18fd5 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -101,6 +101,7 @@ export class BattleActions { // will definitely switch out at this point this.battle.singleEvent('End', oldActive.getAbility(), oldActive.abilityState, oldActive); + this.battle.field.removeSourceFromPseudoWeather(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 diff --git a/sim/battle.ts b/sim/battle.ts index 7a4d67aa65..7888836c7d 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -599,16 +599,16 @@ export class Battle { // it's changed; call it off return relayVar; } - if (eventid === 'SwitchIn' && effect.effectType === 'Ability' && effect.flags['breakable'] && - this.suppressingAbility(target as Pokemon)) { - this.debug(eventid + ' handler suppressed by Mold Breaker'); - return relayVar; - } if (eventid !== 'Start' && eventid !== 'TakeItem' && eventid !== 'SetAbility' && effect.effectType === 'Item' && (target instanceof Pokemon) && target.ignoringItem()) { this.debug(eventid + ' handler suppressed by Embargo, Klutz or Magic Room'); return relayVar; } + if ((['SwitchIn', 'Start', 'End', 'SwitchOut']).includes(eventid) && effect.effectType === 'Ability' && + effect.flags['breakable'] && this.suppressingAbility(target as Pokemon)) { + this.debug(eventid + ' handler suppressed by Mold Breaker'); + return relayVar; + } if (eventid !== 'End' && effect.effectType === 'Ability' && (target instanceof Pokemon) && target.ignoringAbility()) { this.debug(eventid + ' handler suppressed by Gastro Acid or Neutralizing Gas'); return relayVar; diff --git a/sim/dex-moves.ts b/sim/dex-moves.ts index f159dc1155..629d6f6ef5 100644 --- a/sim/dex-moves.ts +++ b/sim/dex-moves.ts @@ -339,10 +339,6 @@ export interface ActiveMove extends MutableMove { totalDamage?: number | false; typeChangerBoosted?: Effect; infiltrates?: boolean; - ruinedAtk?: Pokemon; - ruinedDef?: Pokemon; - ruinedSpA?: Pokemon; - ruinedSpD?: Pokemon; /** * Has this move been boosted by a Z-crystal or used by a Dynamax Pokemon? Usually the same as diff --git a/sim/field.ts b/sim/field.ts index eb01c17b15..9e69dc05fc 100644 --- a/sim/field.ts +++ b/sim/field.ts @@ -229,6 +229,51 @@ export class Field { return true; } + /** + * Start a field effect if it is not already active and maintain a list of sources. + * The field effect is only removed when all sources deactivate it. + */ + addSourcedPseudoWeather( + status: string | Condition, + source: Pokemon, + sourceEffect: Effect | null = null + ): boolean { + const returnValue = this.addPseudoWeather(status, source, sourceEffect); + status = this.battle.dex.conditions.get(status); + const state = this.pseudoWeather[status.id]; + if (state) { + if (!state.activeSources) state.activeSources = []; + state.activeSources.push(source); + } + return returnValue; + } + + /** + * Remove a source from a field effect. If no sources remain, the effect is removed. + */ + removePseudoWeatherSource(status: string | Effect, source: Pokemon) { + status = this.battle.dex.conditions.get(status); + const state = this.pseudoWeather[status.id]; + if (!state) return false; + if (!state.activeSources) throw new Error(`removing pseudoweather without a source`); + state.activeSources = state.activeSources.filter((s: Pokemon) => s !== source); + if (state.activeSources.length) return false; + delete this.pseudoWeather[status.id]; + this.battle.singleEvent('FieldEnd', status, state, this); + return true; + } + + /** + * Remove a source from all active field effects. Used when a Pokemon leaves the field. + */ + removeSourceFromPseudoWeather(source: Pokemon) { + for (const id in this.pseudoWeather) { + if (this.pseudoWeather[id].activeSources) { + this.removePseudoWeatherSource(id, source); + } + } + } + destroy() { // deallocate ourself diff --git a/sim/pokemon.ts b/sim/pokemon.ts index b9283feb3a..8df038f264 100644 --- a/sim/pokemon.ts +++ b/sim/pokemon.ts @@ -870,15 +870,9 @@ export class Pokemon { if (this.getAbility().flags['cantsuppress']) return false; if (this.volatiles['gastroacid']) return true; - // Check if any active pokemon have the ability Neutralizing Gas if (this.hasItem('Ability Shield') || this.ability === ('neutralizinggas' as ID)) return false; - for (const pokemon of this.battle.getAllActive()) { - // can't use hasAbility because it would lead to infinite recursion - if (pokemon.ability === ('neutralizinggas' as ID) && !pokemon.volatiles['gastroacid'] && - !pokemon.transformed && !pokemon.abilityState.ending && !this.volatiles['commanding']) { - return true; - } - } + if (this.volatiles['commanding']) return false; + if (this.battle.field.getPseudoWeather('neutralizinggas')) return true; return false; } diff --git a/test/sim/abilities/swordofruin.js b/test/sim/abilities/swordofruin.js index c795977796..5b8e19ae38 100644 --- a/test/sim/abilities/swordofruin.js +++ b/test/sim/abilities/swordofruin.js @@ -23,7 +23,7 @@ describe(`Sword of Ruin`, () => { }); it(`should not lower the Defense of other Pokemon with the Sword of Ruin Ability`, () => { - battle = common.createBattle({ forceRandomChance: false }, [[ + battle = common.createBattle([[ { species: 'wynaut', ability: 'swordofruin', moves: ['sleeptalk'] }, ], [ { species: 'chienpao', ability: 'swordofruin', moves: ['aerialace'] }, @@ -33,4 +33,78 @@ describe(`Sword of Ruin`, () => { const damage = wynaut.maxhp - wynaut.hp; assert.bounded(damage, [90, 107]); }); + + it(`effect should end if its ability is changed`, () => { + battle = common.createBattle([[ + { species: 'wynaut', ability: 'shellarmor', moves: ['worryseed'] }, + ], [ + { species: 'chienpao', ability: 'swordofruin', moves: ['aerialace'] }, + ]]); + battle.makeChoices(); + const wynaut = battle.p1.active[0]; + const hp = wynaut.hp; + let damage = wynaut.maxhp - hp; + assert.bounded(damage, [120, 142]); + + battle.makeChoices(); + damage = hp - wynaut.hp; + assert.bounded(damage, [90, 107]); + }); + + it(`effect should not end if its ability is changed by a Mold Breaker move, but should end after switching out`, () => { + battle = common.createBattle([[ + { species: 'wynaut', ability: 'moldbreaker', moves: ['worryseed', 'recover'] }, + ], [ + { species: 'chienpao', ability: 'swordofruin', moves: ['aerialace'] }, + { species: 'weavile', moves: ['aerialace'] }, + ]]); + battle.makeChoices(); + const wynaut = battle.p1.active[0]; + let damage = wynaut.maxhp - wynaut.hp; + assert.bounded(damage, [120, 142]); + + let hp = wynaut.hp; + battle.makeChoices(); + damage = hp - wynaut.hp; + assert.bounded(damage, [120, 142]); + + battle.makeChoices('move recover', 'switch weavile'); + + hp = wynaut.hp; + battle.makeChoices(); + damage = hp - wynaut.hp; + assert.bounded(damage, [90, 107]); + }); + + it(`should not lower the Defense of other Pokemon that lost its ability to a Mold Breaker move`, () => { + battle = common.createBattle([[ + { species: 'wynaut', ability: 'swordofruin', moves: ['sleeptalk'] }, + ], [ + { species: 'magikarp', ability: 'moldbreaker', moves: ['worryseed'] }, + { species: 'chienpao', ability: 'swordofruin', moves: ['aerialace'] }, + ]]); + battle.makeChoices(); + battle.makeChoices('auto', 'switch 2'); + battle.makeChoices(); + const wynaut = battle.p1.active[0]; + const damage = wynaut.maxhp - wynaut.hp; + assert.bounded(damage, [90, 107]); + }); + + it(`should lower the Defense of other Pokemon dragged in by a Mold Breaker move`, () => { + battle = common.createBattle([[ + { species: 'wynaut', ability: 'moldbreaker', moves: ['roar'] }, + { species: 'chienpao', ability: 'swordofruin', moves: ['aerialace'] }, + ], [ + { species: 'wynaut', moves: ['sleeptalk'] }, + { species: 'chienpao', ability: 'swordofruin', moves: ['aerialace', 'sleeptalk'] }, + ]]); + battle.makeChoices(); + battle.makeChoices('switch 2', 'move sleeptalk'); + const regularPao = battle.p1.active[0]; + const moldedPao = battle.p2.active[0]; + battle.makeChoices(); + assert.bounded(regularPao.maxhp - regularPao.hp, [61, 72]); + assert.bounded(moldedPao.maxhp - moldedPao.hp, [81, 96]); + }); });