[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:
parent
9e3b5c636c
commit
3bbe01b288
|
@ -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 {
|
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> {
|
applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean | Promise<boolean> {
|
||||||
return false;
|
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 {
|
applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean {
|
||||||
const targets = pokemon.getOpponents();
|
const targets = pokemon.getOpponents();
|
||||||
if (!targets.length) {
|
if (!targets.length) {
|
||||||
|
@ -1988,18 +2003,53 @@ export class TraceAbAttr extends PostSummonAbAttr {
|
||||||
target = targets[0];
|
target = targets[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wonder Guard is normally uncopiable so has the attribute, but trace specifically can copy it
|
if (
|
||||||
if (target.getAbility().hasAttr(UncopiableAbilityAbAttr) && target.getAbility().id !== Abilities.WONDER_GUARD) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.targetName = target.name;
|
||||||
|
this.targetAbilityName = allAbilities[target.getAbility().id].name;
|
||||||
pokemon.summonData.ability = target.getAbility().id;
|
pokemon.summonData.ability = target.getAbility().id;
|
||||||
|
|
||||||
pokemon.scene.queueMessage(getPokemonMessage(pokemon, ` traced ${target.name}'s\n${allAbilities[target.getAbility().id].name}!`));
|
|
||||||
setAbilityRevealed(target);
|
setAbilityRevealed(target);
|
||||||
|
pokemon.updateInfo();
|
||||||
|
|
||||||
return true;
|
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 {
|
export class PostSummonTransformAbAttr extends PostSummonAbAttr {
|
||||||
|
@ -4086,7 +4136,7 @@ export function initAbilities() {
|
||||||
.attr(DoubleBattleChanceAbAttr)
|
.attr(DoubleBattleChanceAbAttr)
|
||||||
.ignorable(),
|
.ignorable(),
|
||||||
new Ability(Abilities.TRACE, 3)
|
new Ability(Abilities.TRACE, 3)
|
||||||
.attr(TraceAbAttr)
|
.attr(PostSummonCopyAbilityAbAttr)
|
||||||
.attr(UncopiableAbilityAbAttr),
|
.attr(UncopiableAbilityAbAttr),
|
||||||
new Ability(Abilities.HUGE_POWER, 3)
|
new Ability(Abilities.HUGE_POWER, 3)
|
||||||
.attr(BattleStatMultiplierAbAttr, BattleStat.ATK, 2),
|
.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))
|
.attr(VariableMovePowerBoostAbAttr, (user, target, move) => 1 + 0.1 * Math.min(user.isPlayer() ? user.scene.currentBattle.playerFaints : user.scene.currentBattle.enemyFaints, 5))
|
||||||
.partial(),
|
.partial(),
|
||||||
new Ability(Abilities.COSTAR, 9)
|
new Ability(Abilities.COSTAR, 9)
|
||||||
.unimplemented(),
|
.attr(PostSummonCopyAllyStatsAbAttr),
|
||||||
new Ability(Abilities.TOXIC_DEBRIS, 9)
|
new Ability(Abilities.TOXIC_DEBRIS, 9)
|
||||||
.attr(PostDefendApplyArenaTrapTagAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, ArenaTagType.TOXIC_SPIKES)
|
.attr(PostDefendApplyArenaTrapTagAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, ArenaTagType.TOXIC_SPIKES)
|
||||||
.bypassFaint(),
|
.bypassFaint(),
|
||||||
|
|
|
@ -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,
|
||||||
|
);
|
||||||
|
});
|
Loading…
Reference in New Issue