[Ability] Implement Costar Ability (#1218)

* Add unthaw to moves that are missing it

Add unthaw to all damaging fire moves

Add Status Effect overrides for easier testing

clean up comments and readd status cure before fainting

* implement Costar ability, refactor TraceAbAttr to be more generic

* format code, clean up comments

* Revert "Add unthaw to moves that are missing it"

This reverts commit 89494fa0c8.

* clean up comments, remove unused call

* ability now copies negatives changes as well

* separate PostSummonCopy into two different classes

* small refactor of copy ability attrs

* add costar to test suite

* remove abstract declaration from comments

* remove broken import statement

* actually fix broken imports
This commit is contained in:
Dmitriy K 2024-06-22 11:35:25 -04:00 committed by GitHub
parent 9e3b5c636c
commit 3bbe01b288
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 148 additions and 7 deletions

View File

@ -1702,7 +1702,18 @@ export class PostIntimidateStatChangeAbAttr extends AbAttr {
}
}
/**
* Base class for defining all {@linkcode Ability} Attributes post summon
* @see {@linkcode applyPostSummon()}
*/
export class PostSummonAbAttr extends AbAttr {
/**
* Applies ability post summon (after switching in)
* @param pokemon {@linkcode Pokemon} with this ability
* @param passive Whether this ability is a passive
* @param args Set of unique arguments needed by this attribute
* @returns true if application of the ability succeeds
*/
applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean | Promise<boolean> {
return false;
}
@ -1974,7 +1985,11 @@ export class PostSummonFormChangeAbAttr extends PostSummonAbAttr {
}
}
export class TraceAbAttr extends PostSummonAbAttr {
/** Attempts to copy a pokemon's ability */
export class PostSummonCopyAbilityAbAttr extends PostSummonAbAttr {
private targetName: string;
private targetAbilityName: string;
applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean {
const targets = pokemon.getOpponents();
if (!targets.length) {
@ -1988,18 +2003,53 @@ export class TraceAbAttr extends PostSummonAbAttr {
target = targets[0];
}
// Wonder Guard is normally uncopiable so has the attribute, but trace specifically can copy it
if (target.getAbility().hasAttr(UncopiableAbilityAbAttr) && target.getAbility().id !== Abilities.WONDER_GUARD) {
if (
target.getAbility().hasAttr(UncopiableAbilityAbAttr) &&
// Wonder Guard is normally uncopiable so has the attribute, but Trace specifically can copy it
!(pokemon.hasAbility(Abilities.TRACE) && target.getAbility().id === Abilities.WONDER_GUARD)
) {
return false;
}
this.targetName = target.name;
this.targetAbilityName = allAbilities[target.getAbility().id].name;
pokemon.summonData.ability = target.getAbility().id;
pokemon.scene.queueMessage(getPokemonMessage(pokemon, ` traced ${target.name}'s\n${allAbilities[target.getAbility().id].name}!`));
setAbilityRevealed(target);
pokemon.updateInfo();
return true;
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
return getPokemonMessage(pokemon, ` copied ${this.targetName}'s\n${this.targetAbilityName}!`);
}
}
/** Attempt to copy the stat changes on an ally pokemon */
export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
private allyName: string;
applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean {
if (!pokemon.scene.currentBattle.double) {
return false;
}
const ally = pokemon.getAlly();
if (!ally || ally.summonData.battleStats.every((change) => change === 0)) {
return false;
}
this.allyName = ally.name;
pokemon.summonData.battleStats = ally.summonData.battleStats;
pokemon.updateInfo();
return true;
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
return getPokemonMessage(pokemon, ` copied ${this.allyName}'s stat changes!`);
}
}
export class PostSummonTransformAbAttr extends PostSummonAbAttr {
@ -4086,7 +4136,7 @@ export function initAbilities() {
.attr(DoubleBattleChanceAbAttr)
.ignorable(),
new Ability(Abilities.TRACE, 3)
.attr(TraceAbAttr)
.attr(PostSummonCopyAbilityAbAttr)
.attr(UncopiableAbilityAbAttr),
new Ability(Abilities.HUGE_POWER, 3)
.attr(BattleStatMultiplierAbAttr, BattleStat.ATK, 2),
@ -4941,7 +4991,7 @@ export function initAbilities() {
.attr(VariableMovePowerBoostAbAttr, (user, target, move) => 1 + 0.1 * Math.min(user.isPlayer() ? user.scene.currentBattle.playerFaints : user.scene.currentBattle.enemyFaints, 5))
.partial(),
new Ability(Abilities.COSTAR, 9)
.unimplemented(),
.attr(PostSummonCopyAllyStatsAbAttr),
new Ability(Abilities.TOXIC_DEBRIS, 9)
.attr(PostDefendApplyArenaTrapTagAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, ArenaTagType.TOXIC_SPIKES)
.bypassFaint(),

View File

@ -0,0 +1,91 @@
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
import GameManager from "../utils/gameManager";
import Phaser from "phaser";
import * as Overrides from "#app/overrides";
import { BattleStat } from "#app/data/battle-stat.js";
import { CommandPhase, MessagePhase } from "#app/phases.js";
import { getMovePosition } from "../utils/gameManagerUtils";
import { Abilities } from "#app/enums/abilities.js";
import { Moves } from "#app/enums/moves.js";
import { Species } from "#app/enums/species.js";
const TIMEOUT = 20 * 1000;
describe("Abilities - COSTAR", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
vi.spyOn(Overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.COSTAR);
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.NASTY_PLOT, Moves.CURSE]);
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
});
test(
"ability copies positive stat changes",
async () => {
await game.startBattle([Species.MAGIKARP, Species.MAGIKARP, Species.FLAMIGO]);
let [leftPokemon, rightPokemon] = game.scene.getPlayerField();
expect(leftPokemon).not.toBe(undefined);
expect(rightPokemon).not.toBe(undefined);
game.doAttack(getMovePosition(game.scene, 0, Moves.NASTY_PLOT));
await game.phaseInterceptor.to(CommandPhase);
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
await game.toNextTurn();
expect(leftPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(+2);
expect(rightPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(0);
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
await game.phaseInterceptor.to(CommandPhase);
game.doSwitchPokemon(2);
await game.phaseInterceptor.to(MessagePhase);
[leftPokemon, rightPokemon] = game.scene.getPlayerField();
expect(leftPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(+2);
expect(rightPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(+2);
},
TIMEOUT,
);
test(
"ability copies negative stat changes",
async () => {
vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INTIMIDATE);
await game.startBattle([Species.MAGIKARP, Species.MAGIKARP, Species.FLAMIGO]);
let [leftPokemon, rightPokemon] = game.scene.getPlayerField();
expect(leftPokemon).not.toBe(undefined);
expect(rightPokemon).not.toBe(undefined);
expect(leftPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2);
expect(leftPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2);
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
await game.phaseInterceptor.to(CommandPhase);
game.doSwitchPokemon(2);
await game.phaseInterceptor.to(MessagePhase);
[leftPokemon, rightPokemon] = game.scene.getPlayerField();
expect(leftPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2);
expect(rightPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2);
},
TIMEOUT,
);
});