[Ability] Stall + Mycelium Might (#3484)

* Implemented Stall

* Fixed implementation

* AbAttr Name Change

* Wrote test for Stall

* Update src/test/abilities/stall.test.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update src/data/ability.ts

Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com>

* Updated ability variables and test

* Apply suggestions from code review

Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>

* eslint fixes

* Update src/test/abilities/stall.test.ts

Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com>

* added documentation and implemented mycelium might

* added note on quick claw

* Documentation + Quick Claw implementation

* This is where I would test quick claw-stall/m.m. if i could override modifierstacks

* Forgot to add edits oops

---------

Co-authored-by: Frutescens <info@laptop>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com>
Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>
This commit is contained in:
Mumble 2024-08-13 13:25:58 -07:00 committed by GitHub
parent 73d60f5e6e
commit 513adf779f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 277 additions and 23 deletions

View File

@ -16,7 +16,7 @@ import { TextStyle, addTextObject, getTextColor } from "./ui/text";
import { allMoves } from "./data/move";
import { ModifierPoolType, getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, modifierTypes } from "./modifier/modifier-type";
import AbilityBar from "./ui/ability-bar";
import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, IncrementMovePriorityAbAttr, PostBattleInitAbAttr, applyAbAttrs, applyPostBattleInitAbAttrs } from "./data/ability";
import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, ChangeMovePriorityAbAttr, PostBattleInitAbAttr, applyAbAttrs, applyPostBattleInitAbAttrs } from "./data/ability";
import { allAbilities } from "./data/ability";
import Battle, { BattleType, FixedBattleConfig } from "./battle";
import { GameMode, GameModes, getGameMode } from "./game-mode";
@ -2121,7 +2121,7 @@ export default class BattleScene extends SceneBase {
pushMovePhase(movePhase: MovePhase, priorityOverride?: integer): void {
const movePriority = new Utils.IntegerHolder(priorityOverride !== undefined ? priorityOverride : movePhase.move.getMove().priority);
applyAbAttrs(IncrementMovePriorityAbAttr, movePhase.pokemon, null, movePhase.move.getMove(), movePriority);
applyAbAttrs(ChangeMovePriorityAbAttr, movePhase.pokemon, null, movePhase.move.getMove(), movePriority);
const lowerPriorityPhase = this.phaseQueue.find(p => p instanceof MovePhase && p.move.getMove().priority < movePriority.value);
if (lowerPriorityPhase) {
this.phaseQueue.splice(this.phaseQueue.indexOf(lowerPriorityPhase), 0, movePhase);

View File

@ -525,7 +525,7 @@ export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr {
applyPreDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
const attackPriority = new Utils.IntegerHolder(move.priority);
applyMoveAttrs(IncrementMovePriorityAttr,attacker,null,move,attackPriority);
applyAbAttrs(IncrementMovePriorityAbAttr, attacker, null, move, attackPriority);
applyAbAttrs(ChangeMovePriorityAbAttr, attacker, null, move, attackPriority);
if (move.moveTarget===MoveTarget.USER || move.moveTarget===MoveTarget.NEAR_ALLY) {
return false;
@ -2682,23 +2682,32 @@ export class BlockOneHitKOAbAttr extends AbAttr {
}
}
export class IncrementMovePriorityAbAttr extends AbAttr {
private moveIncrementFunc: (pokemon: Pokemon, move: Move) => boolean;
private increaseAmount: integer;
/**
* This governs abilities that alter the priority of moves
* Abilities: Prankster, Gale Wings, Triage, Mycelium Might, Stall
* Note - Quick Claw has a separate and distinct implementation outside of priority
*/
export class ChangeMovePriorityAbAttr extends AbAttr {
private moveFunc: (pokemon: Pokemon, move: Move) => boolean;
private changeAmount: number;
constructor(moveIncrementFunc: (pokemon: Pokemon, move: Move) => boolean, increaseAmount = 1) {
/**
* @param {(pokemon, move) => boolean} moveFunc applies priority-change to moves within a provided category
* @param {number} changeAmount the amount of priority added or subtracted
*/
constructor(moveFunc: (pokemon: Pokemon, move: Move) => boolean, changeAmount: number) {
super(true);
this.moveIncrementFunc = moveIncrementFunc;
this.increaseAmount = increaseAmount;
this.moveFunc = moveFunc;
this.changeAmount = changeAmount;
}
apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
if (!this.moveIncrementFunc(pokemon, args[0] as Move)) {
if (!this.moveFunc(pokemon, args[0] as Move)) {
return false;
}
(args[1] as Utils.IntegerHolder).value += this.increaseAmount;
(args[1] as Utils.IntegerHolder).value += this.changeAmount;
return true;
}
}
@ -4092,6 +4101,41 @@ export class BypassSpeedChanceAbAttr extends AbAttr {
}
}
/**
* This attribute checks if a Pokemon's move meets a provided condition to determine if the Pokemon can use Quick Claw
* It was created because Pokemon with the ability Mycelium Might cannot access Quick Claw's benefits when using status moves.
*/
export class PreventBypassSpeedChanceAbAttr extends AbAttr {
private condition: ((pokemon: Pokemon, move: Move) => boolean);
/**
* @param {function} condition - checks if a move meets certain conditions
*/
constructor(condition: (pokemon: Pokemon, move: Move) => boolean) {
super(true);
this.condition = condition;
}
/**
* @argument {boolean} bypassSpeed - determines if a Pokemon is able to bypass speed at the moment
* @argument {boolean} canCheckHeldItems - determines if a Pokemon has access to Quick Claw's effects or not
*/
apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
const bypassSpeed = args[0] as Utils.BooleanHolder;
const canCheckHeldItems = args[1] as Utils.BooleanHolder;
const turnCommand = pokemon.scene.currentBattle.turnCommands[pokemon.getBattlerIndex()];
const isCommandFight = turnCommand?.command === Command.FIGHT;
const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null;
if (this.condition(pokemon, move!) && isCommandFight) {
bypassSpeed.value = false;
canCheckHeldItems.value = false;
return false;
}
return true;
}
}
async function applyAbAttrsInternal<TAttr extends AbAttr>(
attrType: Constructor<TAttr>,
pokemon: Pokemon | null,
@ -4613,7 +4657,7 @@ export function initAbilities() {
.attr(AlwaysHitAbAttr)
.attr(DoubleBattleChanceAbAttr),
new Ability(Abilities.STALL, 4)
.unimplemented(),
.attr(ChangeMovePriorityAbAttr, (pokemon, move: Move) => true, -0.5),
new Ability(Abilities.TECHNICIAN, 4)
.attr(MovePowerBoostAbAttr, (user, target, move) => {
const power = new Utils.NumberHolder(move.power);
@ -4790,7 +4834,7 @@ export function initAbilities() {
.attr(TypeImmunityStatChangeAbAttr, Type.GRASS, BattleStat.ATK, 1)
.ignorable(),
new Ability(Abilities.PRANKSTER, 5)
.attr(IncrementMovePriorityAbAttr, (pokemon, move: Move) => move.category === MoveCategory.STATUS),
.attr(ChangeMovePriorityAbAttr, (pokemon, move: Move) => move.category === MoveCategory.STATUS, 1),
new Ability(Abilities.SAND_FORCE, 5)
.attr(MoveTypePowerBoostAbAttr, Type.ROCK, 1.3)
.attr(MoveTypePowerBoostAbAttr, Type.GROUND, 1.3)
@ -4855,7 +4899,7 @@ export function initAbilities() {
.attr(UnsuppressableAbilityAbAttr)
.attr(NoFusionAbilityAbAttr),
new Ability(Abilities.GALE_WINGS, 6)
.attr(IncrementMovePriorityAbAttr, (pokemon, move) => pokemon.isFullHp() && move.type === Type.FLYING),
.attr(ChangeMovePriorityAbAttr, (pokemon, move) => pokemon.isFullHp() && move.type === Type.FLYING, 1),
new Ability(Abilities.MEGA_LAUNCHER, 6)
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.PULSE_MOVE), 1.5),
new Ability(Abilities.GRASS_PELT, 6)
@ -4944,7 +4988,7 @@ export function initAbilities() {
new Ability(Abilities.LIQUID_VOICE, 7)
.attr(MoveTypeChangeAttr, Type.WATER, 1, (user, target, move) => move.hasFlag(MoveFlags.SOUND_BASED)),
new Ability(Abilities.TRIAGE, 7)
.attr(IncrementMovePriorityAbAttr, (pokemon, move) => move.hasFlag(MoveFlags.TRIAGE_MOVE), 3),
.attr(ChangeMovePriorityAbAttr, (pokemon, move) => move.hasFlag(MoveFlags.TRIAGE_MOVE), 3),
new Ability(Abilities.GALVANIZE, 7)
.attr(MoveTypeChangeAttr, Type.ELECTRIC, 1.2, (user, target, move) => move.type === Type.NORMAL),
new Ability(Abilities.SURGE_SURFER, 7)
@ -5299,8 +5343,9 @@ export function initAbilities() {
.partial() // Healing not blocked by Heal Block
.ignorable(),
new Ability(Abilities.MYCELIUM_MIGHT, 9)
.attr(MoveAbilityBypassAbAttr, (pokemon, move: Move) => move.category === MoveCategory.STATUS)
.partial(),
.attr(ChangeMovePriorityAbAttr, (pokemon, move) => move.category === MoveCategory.STATUS, -0.5)
.attr(PreventBypassSpeedChanceAbAttr, (pokemon, move) => move.category === MoveCategory.STATUS)
.attr(MoveAbilityBypassAbAttr, (pokemon, move: Move) => move.category === MoveCategory.STATUS),
new Ability(Abilities.MINDS_EYE, 9)
.attr(IgnoreTypeImmunityAbAttr, Type.GHOST, [Type.NORMAL, Type.FIGHTING])
.attr(ProtectStatAbAttr, BattleStat.ACC)

View File

@ -2,7 +2,7 @@ import Pokemon from "../field/pokemon";
import Move from "./move";
import { Type } from "./type";
import * as Utils from "../utils";
import { IncrementMovePriorityAbAttr, applyAbAttrs } from "./ability";
import { ChangeMovePriorityAbAttr, applyAbAttrs } from "./ability";
import { ProtectAttr } from "./move";
import { BattlerIndex } from "#app/battle.js";
import i18next from "i18next";
@ -59,7 +59,7 @@ export class Terrain {
case TerrainType.PSYCHIC:
if (!move.hasAttr(ProtectAttr)) {
const priority = new Utils.IntegerHolder(move.priority);
applyAbAttrs(IncrementMovePriorityAbAttr, user, null, move, priority);
applyAbAttrs(ChangeMovePriorityAbAttr, user, null, move, priority);
// Cancels move if the move has positive priority and targets a Pokemon grounded on the Psychic Terrain
return priority.value > 0 && user.getOpponents().some(o => targets.includes(o.getBattlerIndex()) && o.isGrounded());
}

View File

@ -25,7 +25,7 @@ import { Starter } from "./ui/starter-select-ui-handler";
import { Gender } from "./data/gender";
import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather";
import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag";
import { CheckTrappedAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr } from "./data/ability";
import { CheckTrappedAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, ChangeMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, PreventBypassSpeedChanceAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr } from "./data/ability";
import { Unlockables, getUnlockableName } from "./system/unlockables";
import { getBiomeKey } from "./field/arena";
import { BattleType, BattlerIndex, TurnCommand } from "./battle";
@ -2315,8 +2315,12 @@ export class TurnStartPhase extends FieldPhase {
this.scene.getField(true).filter(p => p.summonData).map(p => {
const bypassSpeed = new Utils.BooleanHolder(false);
const canCheckHeldItems = new Utils.BooleanHolder(true);
applyAbAttrs(BypassSpeedChanceAbAttr, p, null, bypassSpeed);
this.scene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed);
applyAbAttrs(PreventBypassSpeedChanceAbAttr, p, null, bypassSpeed, canCheckHeldItems);
if (canCheckHeldItems.value) {
this.scene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed);
}
battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed;
});
@ -2342,10 +2346,15 @@ export class TurnStartPhase extends FieldPhase {
applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a)!, null, aMove, aPriority); //TODO: is the bang correct here?
applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b)!, null, bMove, bPriority); //TODO: is the bang correct here?
applyAbAttrs(IncrementMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a)!, null, aMove, aPriority); //TODO: is the bang correct here?
applyAbAttrs(IncrementMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b)!, null, bMove, bPriority); //TODO: is the bang correct here?
applyAbAttrs(ChangeMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a)!, null, aMove, aPriority); //TODO: is the bang correct here?
applyAbAttrs(ChangeMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b)!, null, bMove, bPriority); //TODO: is the bang correct here?
if (aPriority.value !== bPriority.value) {
const bracketDifference = Math.ceil(aPriority.value) - Math.ceil(bPriority.value);
const hasSpeedDifference = battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value;
if (bracketDifference === 0 && hasSpeedDifference) {
return battlerBypassSpeed[a].value ? -1 : 1;
}
return aPriority.value < bPriority.value ? 1 : -1;
}
}

View File

@ -0,0 +1,105 @@
import { MovePhase, TurnEndPhase } from "#app/phases";
import GameManager from "#test/utils/gameManager";
import { getMovePosition } from "#test/utils/gameManagerUtils";
import { Abilities } from "#enums/abilities";
import { BattleStat } from "#app/data/battle-stat";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Mycelium Might", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.battleType("single");
game.override.disableCrits();
game.override.enemySpecies(Species.SHUCKLE);
game.override.enemyAbility(Abilities.CLEAR_BODY);
game.override.enemyMoveset([Moves.QUICK_ATTACK, Moves.QUICK_ATTACK, Moves.QUICK_ATTACK, Moves.QUICK_ATTACK]);
game.override.ability(Abilities.MYCELIUM_MIGHT);
game.override.moveset([Moves.QUICK_ATTACK, Moves.BABY_DOLL_EYES]);
});
/**
* Bulbapedia References:
* https://bulbapedia.bulbagarden.net/wiki/Mycelium_Might_(Ability)
* https://bulbapedia.bulbagarden.net/wiki/Priority
* https://www.smogon.com/forums/threads/scarlet-violet-battle-mechanics-research.3709545/page-24
**/
it("If a Pokemon with Mycelium Might uses a status move, it will always move last but the status move will ignore protective abilities", async() => {
await game.startBattle([ Species.REGIELEKI ]);
const leadIndex = game.scene.getPlayerPokemon()!.getBattlerIndex();
const enemyPokemon = game.scene.getEnemyPokemon();
const enemyIndex = enemyPokemon?.getBattlerIndex();
game.doAttack(getMovePosition(game.scene, 0, Moves.BABY_DOLL_EYES));
await game.phaseInterceptor.to(MovePhase, false);
// The opponent Pokemon (without Mycelium Might) goes first despite having lower speed than the player Pokemon.
expect((game.scene.getCurrentPhase() as MovePhase).pokemon.getBattlerIndex()).toBe(enemyIndex);
await game.phaseInterceptor.run(MovePhase);
await game.phaseInterceptor.to(MovePhase, false);
// The player Pokemon (with Mycelium Might) goes last despite having higher speed than the opponent.
expect((game.scene.getCurrentPhase() as MovePhase).pokemon.getBattlerIndex()).toBe(leadIndex);
await game.phaseInterceptor.to(TurnEndPhase);
expect(enemyPokemon?.summonData.battleStats[BattleStat.ATK]).toBe(-1);
}, 20000);
it("Pokemon with Mycelium Might will go first if a status move that is in a higher priority bracket than the opponent's move is used", async() => {
game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]);
await game.startBattle([ Species.REGIELEKI ]);
const leadIndex = game.scene.getPlayerPokemon()!.getBattlerIndex();
const enemyPokemon = game.scene.getEnemyPokemon();
const enemyIndex = enemyPokemon?.getBattlerIndex();
game.doAttack(getMovePosition(game.scene, 0, Moves.BABY_DOLL_EYES));
await game.phaseInterceptor.to(MovePhase, false);
// The player Pokemon (with M.M.) goes first because its move is still within a higher priority bracket than its opponent.
expect((game.scene.getCurrentPhase() as MovePhase).pokemon.getBattlerIndex()).toBe(leadIndex);
await game.phaseInterceptor.run(MovePhase);
await game.phaseInterceptor.to(MovePhase, false);
// The enemy Pokemon goes second because its move is in a lower priority bracket.
expect((game.scene.getCurrentPhase() as MovePhase).pokemon.getBattlerIndex()).toBe(enemyIndex);
await game.phaseInterceptor.to(TurnEndPhase);
expect(enemyPokemon?.summonData.battleStats[BattleStat.ATK]).toBe(-1);
}, 20000);
it("Order is established normally if the Pokemon uses a non-status move", async() => {
await game.startBattle([ Species.REGIELEKI ]);
const leadIndex = game.scene.getPlayerPokemon()!.getBattlerIndex();
const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex();
game.doAttack(getMovePosition(game.scene, 0, Moves.QUICK_ATTACK));
await game.phaseInterceptor.to(MovePhase, false);
// The player Pokemon (with M.M.) goes first because it has a higher speed and did not use a status move.
expect((game.scene.getCurrentPhase() as MovePhase).pokemon.getBattlerIndex()).toBe(leadIndex);
await game.phaseInterceptor.run(MovePhase);
await game.phaseInterceptor.to(MovePhase, false);
// The enemy Pokemon (without M.M.) goes second because its speed is lower.
expect((game.scene.getCurrentPhase() as MovePhase).pokemon.getBattlerIndex()).toBe(enemyIndex);
}, 20000);
});

View File

@ -0,0 +1,95 @@
import { MovePhase } from "#app/phases";
import GameManager from "#test/utils/gameManager";
import { getMovePosition } from "#test/utils/gameManagerUtils";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Stall", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.battleType("single");
game.override.disableCrits();
game.override.enemySpecies(Species.REGIELEKI);
game.override.enemyAbility(Abilities.STALL);
game.override.enemyMoveset([Moves.QUICK_ATTACK, Moves.QUICK_ATTACK, Moves.QUICK_ATTACK, Moves.QUICK_ATTACK]);
game.override.moveset([Moves.QUICK_ATTACK, Moves.TACKLE]);
});
/**
* Bulbapedia References:
* https://bulbapedia.bulbagarden.net/wiki/Stall_(Ability)
* https://bulbapedia.bulbagarden.net/wiki/Priority
**/
it("Pokemon with Stall should move last in its priority bracket regardless of speed", async() => {
await game.startBattle([ Species.SHUCKLE ]);
const leadIndex = game.scene.getPlayerPokemon()!.getBattlerIndex();
const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex();
game.doAttack(getMovePosition(game.scene, 0, Moves.QUICK_ATTACK));
await game.phaseInterceptor.to(MovePhase, false);
// The player Pokemon (without Stall) goes first despite having lower speed than the opponent.
expect((game.scene.getCurrentPhase() as MovePhase).pokemon.getBattlerIndex()).toBe(leadIndex);
await game.phaseInterceptor.run(MovePhase);
await game.phaseInterceptor.to(MovePhase, false);
// The opponent Pokemon (with Stall) goes last despite having higher speed than the player Pokemon.
expect((game.scene.getCurrentPhase() as MovePhase).pokemon.getBattlerIndex()).toBe(enemyIndex);
}, 20000);
it("Pokemon with Stall will go first if a move that is in a higher priority bracket than the opponent's move is used", async() => {
await game.startBattle([ Species.SHUCKLE ]);
const leadIndex = game.scene.getPlayerPokemon()!.getBattlerIndex();
const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex();
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
await game.phaseInterceptor.to(MovePhase, false);
// The opponent Pokemon (with Stall) goes first because its move is still within a higher priority bracket than its opponent.
expect((game.scene.getCurrentPhase() as MovePhase).pokemon.getBattlerIndex()).toBe(enemyIndex);
await game.phaseInterceptor.run(MovePhase);
await game.phaseInterceptor.to(MovePhase, false);
// The player Pokemon goes second because its move is in a lower priority bracket.
expect((game.scene.getCurrentPhase() as MovePhase).pokemon.getBattlerIndex()).toBe(leadIndex);
}, 20000);
it("If both Pokemon have stall and use the same move, speed is used to determine who goes first.", async() => {
game.override.ability(Abilities.STALL);
await game.startBattle([ Species.SHUCKLE ]);
const leadIndex = game.scene.getPlayerPokemon()!.getBattlerIndex();
const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex();
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
await game.phaseInterceptor.to(MovePhase, false);
// The opponent Pokemon (with Stall) goes first because it has a higher speed.
expect((game.scene.getCurrentPhase() as MovePhase).pokemon.getBattlerIndex()).toBe(enemyIndex);
await game.phaseInterceptor.run(MovePhase);
await game.phaseInterceptor.to(MovePhase, false);
// The player Pokemon (with Stall) goes second because its speed is lower.
expect((game.scene.getCurrentPhase() as MovePhase).pokemon.getBattlerIndex()).toBe(leadIndex);
}, 20000);
});