Correctly implement Cud Chew and Slow Start (#11376)
Some checks are pending
Node.js CI / build (18.x) (push) Waiting to run

Co-authored-by: Karthik Bandagonda <32044378+Karthik99999@users.noreply.github.com>
This commit is contained in:
André Bastos Dias 2025-09-07 23:24:34 +01:00 committed by GitHub
parent 8584c3cf50
commit 3d4f34e784
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 194 additions and 86 deletions

View File

@ -740,33 +740,29 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = {
num: 238,
},
cudchew: {
onEatItem(item, pokemon) {
if (item.isBerry && pokemon.addVolatile('cudchew')) {
pokemon.volatiles['cudchew'].berry = item;
onEatItem(item, pokemon, source, effect) {
if (item.isBerry && (!effect || !['bugbite', 'pluck'].includes(effect.id))) {
this.effectState.berry = item;
this.effectState.counter = 2;
// This is needed in case the berry was eaten during residuals, preventing the timer from decreasing this turn
if (!this.queue.peek()) this.effectState.counter--;
}
},
onEnd(pokemon) {
delete pokemon.volatiles['cudchew'];
},
condition: {
noCopy: true,
duration: 2,
onRestart() {
this.effectState.duration = 2;
},
onResidualOrder: 28,
onResidualSubOrder: 2,
onEnd(pokemon) {
if (pokemon.hp) {
const item = this.effectState.berry;
this.add('-activate', pokemon, 'ability: Cud Chew');
this.add('-enditem', pokemon, item.name, '[eat]');
if (this.singleEvent('Eat', item, null, pokemon, null, null)) {
this.runEvent('EatItem', pokemon, null, null, item);
}
if (item.onEat) pokemon.ateBerry = true;
onResidualOrder: 28,
onResidualSubOrder: 2,
onResidual(pokemon) {
if (!this.effectState.berry || !pokemon.hp) return;
if (--this.effectState.counter <= 0) {
const item = this.effectState.berry;
this.add('-activate', pokemon, 'ability: Cud Chew');
this.add('-enditem', pokemon, item.name, '[eat]');
if (this.singleEvent('Eat', item, null, pokemon, null, null)) {
this.runEvent('EatItem', pokemon, null, null, item);
}
},
if (item.onEat) pokemon.ateBerry = true;
delete this.effectState.berry;
delete this.effectState.counter;
}
},
flags: {},
name: "Cud Chew",
@ -4233,34 +4229,30 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = {
},
slowstart: {
onStart(pokemon) {
pokemon.addVolatile('slowstart');
this.add('-start', pokemon, 'ability: Slow Start');
this.effectState.counter = 5;
},
onEnd(pokemon) {
delete pokemon.volatiles['slowstart'];
this.add('-end', pokemon, 'Slow Start', '[silent]');
},
condition: {
duration: 5,
onResidualOrder: 28,
onResidualSubOrder: 2,
onStart(target) {
this.add('-start', target, 'ability: Slow Start');
},
onResidual(pokemon) {
if (!pokemon.activeTurns) {
this.effectState.duration! += 1;
onResidualOrder: 28,
onResidualSubOrder: 2,
onResidual(pokemon) {
if (pokemon.activeTurns && this.effectState.counter) {
this.effectState.counter--;
if (!this.effectState.counter) {
this.add('-end', pokemon, 'Slow Start');
delete this.effectState.counter;
}
},
onModifyAtkPriority: 5,
onModifyAtk(atk, pokemon) {
}
},
onModifyAtkPriority: 5,
onModifyAtk(atk, pokemon) {
if (this.effectState.counter) {
return this.chainModify(0.5);
},
onModifySpe(spe, pokemon) {
}
},
onModifySpe(spe, pokemon) {
if (this.effectState.counter) {
return this.chainModify(0.5);
},
onEnd(target) {
this.add('-end', target, 'Slow Start');
},
}
},
flags: {},
name: "Slow Start",

View File

@ -550,8 +550,8 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
this.debug(`BP: ${move.basePower}`);
if (item.isBerry) {
move.onHit = function (foe) {
if (this.singleEvent('Eat', item, null, foe, null, null)) {
this.runEvent('EatItem', foe, null, null, item);
if (this.singleEvent('Eat', item, source.itemState, foe, source, move)) {
this.runEvent('EatItem', foe, source, move, item);
if (item.id === 'leppaberry') foe.staleness = 'external';
}
if (item.onEat) foe.ateBerry = true;

View File

@ -87,33 +87,18 @@ export const Abilities: import('../../../sim/dex-abilities').ModdedAbilityDataTa
},
slowstart: {
inherit: true,
condition: {
duration: 5,
onResidualOrder: 28,
onResidualSubOrder: 2,
onStart(target) {
this.add('-start', target, 'ability: Slow Start');
},
onModifyAtkPriority: 5,
onModifyAtk(atk, pokemon, target, move) {
// This is because the game checks the move's category in data, rather than what it is currently, unlike e.g. Huge Power
if (this.dex.moves.get(move.id).category === 'Physical') {
return this.chainModify(0.5);
}
},
onModifySpAPriority: 5,
onModifySpA(spa, pokemon, target, move) {
// Ordinary Z-moves like Breakneck Blitz will halve the user's Special Attack as well
if (this.dex.moves.get(move.id).category === 'Physical') {
return this.chainModify(0.5);
}
},
onModifySpe(spe, pokemon) {
onModifyAtk(atk, pokemon, target, move) {
// This is because the game checks the move's category in data, rather than what it is currently, unlike e.g. Huge Power
if (this.effectState.counter && this.dex.moves.get(move.id).category === 'Physical') {
return this.chainModify(0.5);
},
onEnd(target) {
this.add('-end', target, 'Slow Start');
},
}
},
onModifySpAPriority: 5,
onModifySpA(spa, pokemon, target, move) {
// Ordinary Z-moves like Breakneck Blitz will halve the user's Special Attack as well
if (this.effectState.counter && this.dex.moves.get(move.id).category === 'Physical') {
return this.chainModify(0.5);
}
},
},
soundproof: {

View File

@ -1982,12 +1982,12 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = {
pp: 20,
priority: 0,
flags: { contact: 1, protect: 1, mirror: 1, metronome: 1 },
onHit(target, source) {
onHit(target, source, move) {
const item = target.getItem();
if (source.hp && item.isBerry && target.takeItem(source)) {
this.add('-enditem', target, item.name, '[from] stealeat', '[move] Bug Bite', `[of] ${source}`);
if (this.singleEvent('Eat', item, null, source, null, null)) {
this.runEvent('EatItem', source, null, null, item);
if (this.singleEvent('Eat', item, target.itemState, source, source, move)) {
this.runEvent('EatItem', source, source, move, item);
if (item.id === 'leppaberry') target.staleness = 'external';
}
if (item.onEat) source.ateBerry = true;
@ -5950,9 +5950,12 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = {
move.basePower = item.fling.basePower;
this.debug(`BP: ${move.basePower}`);
if (item.isBerry) {
if (source.hasAbility('cudchew')) {
this.singleEvent('EatItem', source.getAbility(), source.abilityState, source, source, move, item);
}
move.onHit = function (foe) {
if (this.singleEvent('Eat', item, null, foe, null, null)) {
this.runEvent('EatItem', foe, null, null, item);
if (this.singleEvent('Eat', item, source.itemState, foe, source, move)) {
this.runEvent('EatItem', foe, source, move, item);
if (item.id === 'leppaberry') foe.staleness = 'external';
}
if (item.onEat) foe.ateBerry = true;
@ -13918,12 +13921,12 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = {
pp: 20,
priority: 0,
flags: { contact: 1, protect: 1, mirror: 1, distance: 1, metronome: 1 },
onHit(target, source) {
onHit(target, source, move) {
const item = target.getItem();
if (source.hp && item.isBerry && target.takeItem(source)) {
this.add('-enditem', target, item.name, '[from] stealeat', '[move] Pluck', `[of] ${source}`);
if (this.singleEvent('Eat', item, null, source, null, null)) {
this.runEvent('EatItem', source, null, null, item);
if (this.singleEvent('Eat', item, target.itemState, source, source, move)) {
this.runEvent('EatItem', source, source, move, item);
if (item.id === 'leppaberry') target.staleness = 'external';
}
if (item.onEat) source.ateBerry = true;

View File

@ -48,7 +48,7 @@ export interface EventMethods {
onDeductPP?: (this: Battle, target: Pokemon, source: Pokemon) => number | void;
onDisableMove?: (this: Battle, pokemon: Pokemon) => void;
onDragOut?: (this: Battle, pokemon: Pokemon, source?: Pokemon, move?: ActiveMove) => void;
onEatItem?: (this: Battle, item: Item, pokemon: Pokemon) => void;
onEatItem?: (this: Battle, item: Item, pokemon: Pokemon, source?: Pokemon, effect?: Effect) => void;
onEffectiveness?: MoveEventMethods['onEffectiveness'];
onEntryHazard?: (this: Battle, pokemon: Pokemon) => void;
onFaint?: CommonHandlers['VoidEffect'];

View File

@ -1751,7 +1751,7 @@ export class Pokemon {
this.battle.add('-enditem', this, item, '[eat]');
this.battle.singleEvent('Eat', item, this.itemState, this, source, sourceEffect);
this.battle.runEvent('EatItem', this, null, null, item);
this.battle.runEvent('EatItem', this, source, sourceEffect, item);
if (RESTORATIVE_BERRIES.has(item.id)) {
switch (this.pendingStaleness) {

View File

@ -0,0 +1,91 @@
'use strict';
const assert = require('./../../assert');
const common = require('./../../common');
let battle;
describe('Cud Chew', () => {
afterEach(() => {
battle.destroy();
});
it(`should re-activate a berry, eaten in the previous turn`, () => {
battle = common.createBattle([[
{ species: 'tauros', ability: 'cudchew', item: 'lumberry', moves: ['sleeptalk'] },
], [
{ species: 'toxicroak', moves: ['toxic'] },
]]);
const tauros = battle.p1.active[0];
battle.makeChoices();
assert.equal(tauros.status, '');
battle.makeChoices();
assert.equal(tauros.status, '');
battle.makeChoices();
assert.equal(tauros.status, 'tox');
});
it(`should re-activate a berry flung in the previous turn, for both the attacker and the target`, () => {
battle = common.createBattle({ gameType: 'doubles' }, [[
{ species: 'tauros', ability: 'cudchew', item: 'lumberry', moves: ['fling', 'sleeptalk'] },
{ species: 'foongus', moves: ['toxic', 'sleeptalk'] },
], [
{ species: 'farigiraf', ability: 'cudchew', moves: ['sleeptalk'] },
{ species: 'grimer', moves: ['toxic', 'sleeptalk'] },
]]);
const tauros = battle.p1.active[0];
const farigiraf = battle.p2.active[0];
battle.makeChoices('move fling 1, move toxic 1', 'move sleeptalk, move toxic 1');
assert.equal(tauros.status, 'tox');
assert.equal(farigiraf.status, 'tox');
battle.makeChoices('move sleeptalk, move sleeptalk', 'move sleeptalk, move sleeptalk');
assert.equal(tauros.status, '');
assert.equal(farigiraf.status, '');
});
it(`should not re-activate a berry eaten by Bug Bite, for either the attacker or the target`, () => {
battle = common.createBattle({ gameType: 'doubles' }, [[
{ species: 'tauros', ability: 'cudchew', moves: ['bugbite', 'sleeptalk'] },
{ species: 'foongus', moves: ['toxic', 'sleeptalk'] },
], [
{ species: 'farigiraf', ability: 'cudchew', item: 'lumberry', moves: ['sleeptalk'] },
{ species: 'grimer', moves: ['toxic', 'sleeptalk'] },
]]);
const tauros = battle.p1.active[0];
const farigiraf = battle.p2.active[0];
battle.makeChoices('move bugbite 1, move toxic 1', 'move sleeptalk, move toxic 1');
assert.equal(tauros.status, 'tox');
assert.equal(farigiraf.status, 'tox');
battle.makeChoices('move sleeptalk, move sleeptalk', 'move sleeptalk, move sleeptalk');
assert.equal(tauros.status, 'tox');
assert.equal(farigiraf.status, 'tox');
});
it(`should not be prevented by Unnerve`, () => {
battle = common.createBattle({ gameType: 'doubles' }, [[
{ species: 'tauros', ability: 'cudchew', item: 'lumberry', moves: ['sleeptalk'] },
{ species: 'wynaut', moves: ['sleeptalk'] },
], [
{ species: 'toxicroak', moves: ['toxic'] },
{ species: 'magikarp', moves: ['sleeptalk'] },
{ species: 'mewtwo', ability: 'unnerve', moves: ['sleeptalk'] },
]]);
const tauros = battle.p1.active[0];
battle.makeChoices();
assert.equal(tauros.status, '');
battle.makeChoices('auto', 'move toxic 1, switch 3');
assert.equal(tauros.status, '');
});
it(`should still activate in the following turn if the berry was consumed during residuals`, () => {
battle = common.createBattle([[
{ species: 'tauros', ability: 'cudchew', item: 'sitrusberry', moves: ['bellydrum'] },
], [
{ species: 'toxicroak', moves: ['toxic'] },
]]);
const tauros = battle.p1.active[0];
battle.makeChoices();
battle.makeChoices();
assert(tauros.hp > tauros.maxhp * 3 / 4, 'Tauros should have eaten its berry twice');
});
});

View File

@ -274,6 +274,43 @@ describe('Neutralizing Gas', () => {
assert.species(eiscue, 'Eiscue-Noice');
});
it(`should delay the activation of Cud Chew`, () => {
battle = common.createBattle({ gameType: 'doubles' }, [[
{ species: 'tauros', ability: 'cudchew', item: 'lumberry', moves: ['sleeptalk'] },
{ species: 'wynaut', moves: ['sleeptalk'] },
], [
{ species: 'toxicroak', moves: ['toxic'] },
{ species: 'magikarp', moves: ['sleeptalk'] },
{ species: 'weezing', ability: 'neutralizinggas', moves: ['sleeptalk'] },
]]);
let tauros = battle.p1.active[0];
battle.makeChoices();
assert.equal(tauros.status, '');
battle.makeChoices('auto', 'move toxic 1, switch 3');
assert.equal(tauros.status, 'tox');
battle.makeChoices('auto', 'move toxic 1, switch 3');
assert.equal(tauros.status, '');
battle = common.createBattle({ gameType: 'doubles' }, [[
{ species: 'tauros', ability: 'cudchew', item: 'lumberry', moves: ['sleeptalk'] },
{ species: 'wynaut', moves: ['sleeptalk'] },
], [
{ species: 'toxicroak', moves: ['toxic'] },
{ species: 'magikarp', moves: ['sleeptalk', 'uturn'] },
{ species: 'weezing', ability: 'neutralizinggas', moves: ['sleeptalk'] },
]]);
tauros = battle.p1.active[0];
battle.makeChoices('auto', 'move toxic 1, move uturn 2');
battle.makeChoices();
assert.equal(tauros.status, ''); // 0 turns passed before Neutralizing Gas
battle.makeChoices();
assert.equal(tauros.status, 'tox');
battle.makeChoices('auto', 'move toxic 1, switch 3');
assert.equal(tauros.status, 'tox'); // 1 turn has passed
battle.makeChoices();
assert.equal(tauros.status, '');
});
it(`should not work if it was obtained via Transform`, () => {
battle = common.createBattle([[
{ species: 'Ditto', moves: ['transform'] },