This commit is contained in:
André Bastos Dias 2026-06-02 14:06:32 +01:00 committed by GitHub
commit bc35b3f4f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 236 additions and 113 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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