mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-21 00:52:47 +02:00
[Bug][Ability] Fix change move type abilities (#5665)
* Make type changing moves change type after abilities but before ion deluge/electrify * Create unified test file for galvanize, pixilate, and refrigerate * Make type boost items like silk scarf affect the move after its type change * Add tests for type boost item interaction * Remove leftover log messages * Update spies in type-change ability tests * Add automated tests for normalize * Fix test name injection for tera blast * Add automated test for tera blast normalize interaction * Restore pokemon as a type-only import in moves.ts * Add aerilate to type changing tests * Rename galvanize test file * Fix utils import * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Remove unnecessary mockRestore * Remove unnecessary nullish coalescing Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/field/pokemon.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --------- Co-authored-by: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
parent
8eeec9511b
commit
3af1bdbcff
@ -14,3 +14,6 @@ export const MAX_INT_ATTR_VALUE = 0x80000000;
|
||||
export const CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [10, 180] as const;
|
||||
/** The min and max waves for mystery encounters to spawn in challenge mode */
|
||||
export const CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [10, 180] as const;
|
||||
|
||||
/** The raw percentage power boost for type boost items*/
|
||||
export const TYPE_BOOST_ITEM_BOOST_PERCENT = 20;
|
||||
|
@ -14,7 +14,6 @@ import {
|
||||
SelfStatusMove,
|
||||
VariablePowerAttr,
|
||||
applyMoveAttrs,
|
||||
VariableMoveTypeAttr,
|
||||
RandomMovesetMoveAttr,
|
||||
RandomMoveAttr,
|
||||
NaturePowerAttr,
|
||||
@ -73,6 +72,7 @@ import type { BattlerIndex } from "#app/battle";
|
||||
import type Move from "#app/data/moves/move";
|
||||
import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag";
|
||||
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
|
||||
import { noAbilityTypeOverrideMoves } from "../moves/invalid-moves";
|
||||
|
||||
export class BlockRecoilDamageAttr extends AbAttr {
|
||||
constructor() {
|
||||
@ -1240,12 +1240,39 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
|
||||
super(false);
|
||||
}
|
||||
|
||||
override canApplyPreAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon | null, move: Move, args: any[]): boolean {
|
||||
return (this.condition && this.condition(pokemon, defender, move)) ?? false;
|
||||
/**
|
||||
* Determine if the move type change attribute can be applied
|
||||
*
|
||||
* Can be applied if:
|
||||
* - The ability's condition is met, e.g. pixilate only boosts normal moves,
|
||||
* - The move is not forbidden from having its type changed by an ability, e.g. {@linkcode Moves.MULTI_ATTACK}
|
||||
* - The user is not terastallized and using tera blast
|
||||
* - The user is not a terastallized terapagos with tera stellar using tera starstorm
|
||||
* @param pokemon - The pokemon that has the move type changing ability and is using the attacking move
|
||||
* @param _passive - Unused
|
||||
* @param _simulated - Unused
|
||||
* @param _defender - The pokemon being attacked (unused)
|
||||
* @param move - The move being used
|
||||
* @param _args - args[0] holds the type that the move is changed to, args[1] holds the multiplier
|
||||
* @returns whether the move type change attribute can be applied
|
||||
*/
|
||||
override canApplyPreAttack(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _defender: Pokemon | null, move: Move, _args: [NumberHolder?, NumberHolder?, ...any]): boolean {
|
||||
return (!this.condition || this.condition(pokemon, _defender, move)) &&
|
||||
!noAbilityTypeOverrideMoves.has(move.id) &&
|
||||
(!pokemon.isTerastallized ||
|
||||
(move.id !== Moves.TERA_BLAST &&
|
||||
(move.id !== Moves.TERA_STARSTORM || pokemon.getTeraType() !== PokemonType.STELLAR || !pokemon.hasSpecies(Species.TERAPAGOS))));
|
||||
}
|
||||
|
||||
// TODO: Decouple this into two attributes (type change / power boost)
|
||||
override applyPreAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, args: any[]): void {
|
||||
/**
|
||||
* @param pokemon - The pokemon that has the move type changing ability and is using the attacking move
|
||||
* @param passive - Unused
|
||||
* @param simulated - Unused
|
||||
* @param defender - The pokemon being attacked (unused)
|
||||
* @param move - The move being used
|
||||
* @param args - args[0] holds the type that the move is changed to, args[1] holds the multiplier
|
||||
*/
|
||||
override applyPreAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, args: [NumberHolder?, NumberHolder?, ...any]): void {
|
||||
if (args[0] && args[0] instanceof NumberHolder) {
|
||||
args[0].value = this.newType;
|
||||
}
|
||||
@ -6629,9 +6656,7 @@ export function initAbilities() {
|
||||
.conditionalAttr(pokemon => pokemon.status ? pokemon.status.effect === StatusEffect.PARALYSIS : false, StatMultiplierAbAttr, Stat.SPD, 2)
|
||||
.conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), StatMultiplierAbAttr, Stat.SPD, 1.5),
|
||||
new Ability(Abilities.NORMALIZE, 4)
|
||||
.attr(MoveTypeChangeAbAttr, PokemonType.NORMAL, 1.2, (user, target, move) => {
|
||||
return ![ Moves.MULTI_ATTACK, Moves.REVELATION_DANCE, Moves.TERRAIN_PULSE, Moves.HIDDEN_POWER, Moves.WEATHER_BALL, Moves.NATURAL_GIFT, Moves.JUDGMENT, Moves.TECHNO_BLAST ].includes(move.id);
|
||||
}),
|
||||
.attr(MoveTypeChangeAbAttr, PokemonType.NORMAL, 1.2),
|
||||
new Ability(Abilities.SNIPER, 4)
|
||||
.attr(MultCritAbAttr, 1.5),
|
||||
new Ability(Abilities.MAGIC_GUARD, 4)
|
||||
@ -6896,7 +6921,7 @@ export function initAbilities() {
|
||||
new Ability(Abilities.STRONG_JAW, 6)
|
||||
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.BITING_MOVE), 1.5),
|
||||
new Ability(Abilities.REFRIGERATE, 6)
|
||||
.attr(MoveTypeChangeAbAttr, PokemonType.ICE, 1.2, (user, target, move) => move.type === PokemonType.NORMAL && !move.hasAttr(VariableMoveTypeAttr)),
|
||||
.attr(MoveTypeChangeAbAttr, PokemonType.ICE, 1.2, (user, target, move) => move.type === PokemonType.NORMAL),
|
||||
new Ability(Abilities.SWEET_VEIL, 6)
|
||||
.attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.SLEEP)
|
||||
.attr(PostSummonUserFieldRemoveStatusEffectAbAttr, StatusEffect.SLEEP)
|
||||
@ -6920,11 +6945,11 @@ export function initAbilities() {
|
||||
new Ability(Abilities.TOUGH_CLAWS, 6)
|
||||
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), 1.3),
|
||||
new Ability(Abilities.PIXILATE, 6)
|
||||
.attr(MoveTypeChangeAbAttr, PokemonType.FAIRY, 1.2, (user, target, move) => move.type === PokemonType.NORMAL && !move.hasAttr(VariableMoveTypeAttr)),
|
||||
.attr(MoveTypeChangeAbAttr, PokemonType.FAIRY, 1.2, (user, target, move) => move.type === PokemonType.NORMAL),
|
||||
new Ability(Abilities.GOOEY, 6)
|
||||
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), Stat.SPD, -1, false),
|
||||
new Ability(Abilities.AERILATE, 6)
|
||||
.attr(MoveTypeChangeAbAttr, PokemonType.FLYING, 1.2, (user, target, move) => move.type === PokemonType.NORMAL && !move.hasAttr(VariableMoveTypeAttr)),
|
||||
.attr(MoveTypeChangeAbAttr, PokemonType.FLYING, 1.2, (user, target, move) => move.type === PokemonType.NORMAL),
|
||||
new Ability(Abilities.PARENTAL_BOND, 6)
|
||||
.attr(AddSecondStrikeAbAttr, 0.25),
|
||||
new Ability(Abilities.DARK_AURA, 6)
|
||||
@ -7001,7 +7026,7 @@ export function initAbilities() {
|
||||
new Ability(Abilities.TRIAGE, 7)
|
||||
.attr(ChangeMovePriorityAbAttr, (pokemon, move) => move.hasFlag(MoveFlags.TRIAGE_MOVE), 3),
|
||||
new Ability(Abilities.GALVANIZE, 7)
|
||||
.attr(MoveTypeChangeAbAttr, PokemonType.ELECTRIC, 1.2, (user, target, move) => move.type === PokemonType.NORMAL && !move.hasAttr(VariableMoveTypeAttr)),
|
||||
.attr(MoveTypeChangeAbAttr, PokemonType.ELECTRIC, 1.2, (_user, _target, move) => move.type === PokemonType.NORMAL),
|
||||
new Ability(Abilities.SURGE_SURFER, 7)
|
||||
.conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), StatMultiplierAbAttr, Stat.SPD, 2),
|
||||
new Ability(Abilities.SCHOOLING, 7)
|
||||
|
@ -240,3 +240,18 @@ export const invalidMirrorMoveMoves: ReadonlySet<Moves> = new Set([
|
||||
Moves.WATER_SPORT,
|
||||
Moves.WIDE_GUARD,
|
||||
]);
|
||||
|
||||
/** Set of moves that can never have their type overridden by an ability like Pixilate or Normalize
|
||||
*
|
||||
* Excludes tera blast and tera starstorm, as these are only conditionally forbidden
|
||||
*/
|
||||
export const noAbilityTypeOverrideMoves: ReadonlySet<Moves> = new Set([
|
||||
Moves.WEATHER_BALL,
|
||||
Moves.JUDGMENT,
|
||||
Moves.REVELATION_DANCE,
|
||||
Moves.MULTI_ATTACK,
|
||||
Moves.TERRAIN_PULSE,
|
||||
Moves.NATURAL_GIFT,
|
||||
Moves.TECHNO_BLAST,
|
||||
Moves.HIDDEN_POWER,
|
||||
]);
|
||||
|
@ -810,8 +810,9 @@ export default class Move implements Localizable {
|
||||
|
||||
const power = new NumberHolder(this.power);
|
||||
const typeChangeMovePowerMultiplier = new NumberHolder(1);
|
||||
const typeChangeHolder = new NumberHolder(this.type);
|
||||
|
||||
applyPreAttackAbAttrs(MoveTypeChangeAbAttr, source, target, this, true, null, typeChangeMovePowerMultiplier);
|
||||
applyPreAttackAbAttrs(MoveTypeChangeAbAttr, source, target, this, true, typeChangeHolder, typeChangeMovePowerMultiplier);
|
||||
|
||||
const sourceTeraType = source.getTeraType();
|
||||
if (source.isTerastallized && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr(MultiHitAttr) && !globalScene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) {
|
||||
@ -841,7 +842,7 @@ export default class Move implements Localizable {
|
||||
|
||||
power.value *= typeChangeMovePowerMultiplier.value;
|
||||
|
||||
const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === this.type) as TypeBoostTag;
|
||||
const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === typeChangeHolder.value) as TypeBoostTag;
|
||||
if (typeBoost) {
|
||||
power.value *= typeBoost.boostValue;
|
||||
}
|
||||
@ -849,8 +850,8 @@ export default class Move implements Localizable {
|
||||
applyMoveAttrs(VariablePowerAttr, source, target, this, power);
|
||||
|
||||
if (!this.hasAttr(TypelessAttr)) {
|
||||
globalScene.arena.applyTags(WeakenMoveTypeTag, simulated, this.type, power);
|
||||
globalScene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, this.type, power);
|
||||
globalScene.arena.applyTags(WeakenMoveTypeTag, simulated, typeChangeHolder.value, power);
|
||||
globalScene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, typeChangeHolder.value, power);
|
||||
}
|
||||
|
||||
if (source.getTag(HelpingHandTag)) {
|
||||
@ -4826,8 +4827,13 @@ export class FormChangeItemTypeAttr extends VariableMoveTypeAttr {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Force move to have its original typing if it changed
|
||||
if (moveType.value === move.type) {
|
||||
return false;
|
||||
}
|
||||
moveType.value = move.type
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class TechnoBlastTypeAttr extends VariableMoveTypeAttr {
|
||||
@ -4977,8 +4983,12 @@ export class WeatherBallTypeAttr extends VariableMoveTypeAttr {
|
||||
moveType.value = PokemonType.ICE;
|
||||
break;
|
||||
default:
|
||||
if (moveType.value === move.type) {
|
||||
return false;
|
||||
}
|
||||
moveType.value = move.type;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -5025,8 +5035,13 @@ export class TerrainPulseTypeAttr extends VariableMoveTypeAttr {
|
||||
moveType.value = PokemonType.PSYCHIC;
|
||||
break;
|
||||
default:
|
||||
if (moveType.value === move.type) {
|
||||
return false;
|
||||
}
|
||||
// force move to have its original typing if it was changed
|
||||
moveType.value = move.type;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -2591,9 +2591,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
null,
|
||||
move,
|
||||
simulated,
|
||||
moveTypeHolder,
|
||||
moveTypeHolder
|
||||
);
|
||||
|
||||
// If the user is terastallized and the move is tera blast, or tera starstorm that is stellar type,
|
||||
// then bypass the check for ion deluge and electrify
|
||||
if (this.isTerastallized && (move.id === Moves.TERA_BLAST || move.id === Moves.TERA_STARSTORM && moveTypeHolder.value === PokemonType.STELLAR)) {
|
||||
return moveTypeHolder.value as PokemonType;
|
||||
}
|
||||
|
||||
globalScene.arena.applyTags(
|
||||
ArenaTagType.ION_DELUGE,
|
||||
simulated,
|
||||
|
@ -128,6 +128,7 @@ import { getStatKey, Stat, TEMP_BATTLE_STATS } from "#enums/stat";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import i18next from "i18next";
|
||||
import { timedEventManager } from "#app/global-event-manager";
|
||||
import { TYPE_BOOST_ITEM_BOOST_PERCENT } from "#app/constants";
|
||||
|
||||
const outputModifierData = false;
|
||||
const useMaxWeightForOutput = false;
|
||||
@ -1329,7 +1330,7 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator {
|
||||
constructor() {
|
||||
super((party: Pokemon[], pregenArgs?: any[]) => {
|
||||
if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in PokemonType) {
|
||||
return new AttackTypeBoosterModifierType(pregenArgs[0] as PokemonType, 20);
|
||||
return new AttackTypeBoosterModifierType(pregenArgs[0] as PokemonType, TYPE_BOOST_ITEM_BOOST_PERCENT);
|
||||
}
|
||||
|
||||
const attackMoveTypes = party.flatMap(p =>
|
||||
@ -1377,7 +1378,7 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator {
|
||||
weight += typeWeight;
|
||||
}
|
||||
|
||||
return new AttackTypeBoosterModifierType(type!, 20);
|
||||
return new AttackTypeBoosterModifierType(type!, TYPE_BOOST_ITEM_BOOST_PERCENT);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1479,7 +1479,8 @@ export class AttackTypeBoosterModifier extends PokemonHeldItemModifier {
|
||||
return (
|
||||
super.shouldApply(pokemon, moveType, movePower) &&
|
||||
typeof moveType === "number" &&
|
||||
movePower instanceof NumberHolder
|
||||
movePower instanceof NumberHolder &&
|
||||
this.moveType === moveType
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,131 +0,0 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { allMoves } from "#app/data/moves/move";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { Species } from "#app/enums/species";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Abilities - Galvanize", () => {
|
||||
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
|
||||
.battleStyle("single")
|
||||
.startingLevel(100)
|
||||
.ability(Abilities.GALVANIZE)
|
||||
.moveset([Moves.TACKLE, Moves.REVELATION_DANCE, Moves.FURY_SWIPES])
|
||||
.enemySpecies(Species.DUSCLOPS)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
it("should change Normal-type attacks to Electric type and boost their power", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(playerPokemon, "getMoveType");
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
|
||||
|
||||
const move = allMoves[Moves.TACKLE];
|
||||
vi.spyOn(move, "calculateBattlePower");
|
||||
|
||||
game.move.select(Moves.TACKLE);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(playerPokemon.getMoveType).toHaveLastReturnedWith(PokemonType.ELECTRIC);
|
||||
expect(spy).toHaveReturnedWith(1);
|
||||
expect(move.calculateBattlePower).toHaveReturnedWith(48);
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("should cause Normal-type attacks to activate Volt Absorb", async () => {
|
||||
game.override.enemyAbility(Abilities.VOLT_ABSORB);
|
||||
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(playerPokemon, "getMoveType");
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
|
||||
|
||||
enemyPokemon.hp = Math.floor(enemyPokemon.getMaxHp() * 0.8);
|
||||
|
||||
game.move.select(Moves.TACKLE);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(playerPokemon.getMoveType).toHaveLastReturnedWith(PokemonType.ELECTRIC);
|
||||
expect(spy).toHaveReturnedWith(0);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
});
|
||||
|
||||
it("should not change the type of variable-type moves", async () => {
|
||||
game.override.enemySpecies(Species.MIGHTYENA);
|
||||
|
||||
await game.classicMode.startBattle([Species.ESPEON]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(playerPokemon, "getMoveType");
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.REVELATION_DANCE);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(playerPokemon.getMoveType).not.toHaveLastReturnedWith(PokemonType.ELECTRIC);
|
||||
expect(spy).toHaveReturnedWith(0);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
});
|
||||
|
||||
it("should affect all hits of a Normal-type multi-hit move", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(playerPokemon, "getMoveType");
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.FURY_SWIPES);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.move.forceHit();
|
||||
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
expect(playerPokemon.turnData.hitCount).toBeGreaterThan(1);
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
|
||||
while (playerPokemon.turnData.hitsLeft > 0) {
|
||||
const enemyStartingHp = enemyPokemon.hp;
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(playerPokemon.getMoveType).toHaveLastReturnedWith(PokemonType.ELECTRIC);
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
|
||||
}
|
||||
|
||||
expect(spy).not.toHaveReturnedWith(0);
|
||||
});
|
||||
});
|
190
test/abilities/normal-move-type-change.test.ts
Normal file
190
test/abilities/normal-move-type-change.test.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { allMoves } from "#app/data/moves/move";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { Species } from "#app/enums/species";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { TYPE_BOOST_ITEM_BOOST_PERCENT } from "#app/constants";
|
||||
import { allAbilities } from "#app/data/data-lists";
|
||||
import { MoveTypeChangeAbAttr } from "#app/data/abilities/ability";
|
||||
import { toDmgValue } from "#app/utils/common";
|
||||
|
||||
/**
|
||||
* Tests for abilities that change the type of normal moves to
|
||||
* a different type and boost their power
|
||||
*
|
||||
* Includes
|
||||
* - Aerialate
|
||||
* - Galvanize
|
||||
* - Pixilate
|
||||
* - Refrigerate
|
||||
*/
|
||||
|
||||
describe.each([
|
||||
{ ab: Abilities.GALVANIZE, ab_name: "Galvanize", ty: PokemonType.ELECTRIC, tyName: "electric" },
|
||||
{ ab: Abilities.PIXILATE, ab_name: "Pixilate", ty: PokemonType.FAIRY, tyName: "fairy" },
|
||||
{ ab: Abilities.REFRIGERATE, ab_name: "Refrigerate", ty: PokemonType.ICE, tyName: "ice" },
|
||||
{ ab: Abilities.AERILATE, ab_name: "Aerilate", ty: PokemonType.FLYING, tyName: "flying" },
|
||||
])("Abilities - $ab_name", ({ ab, ty, tyName }) => {
|
||||
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
|
||||
.battleStyle("single")
|
||||
.startingLevel(100)
|
||||
.starterSpecies(Species.MAGIKARP)
|
||||
.ability(ab)
|
||||
.moveset([Moves.TACKLE, Moves.REVELATION_DANCE, Moves.FURY_SWIPES])
|
||||
.enemySpecies(Species.DUSCLOPS)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
it(`should change Normal-type attacks to ${tyName} type and boost their power`, async () => {
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const typeSpy = vi.spyOn(playerPokemon, "getMoveType");
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const enemySpy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
|
||||
const powerSpy = vi.spyOn(allMoves[Moves.TACKLE], "calculateBattlePower");
|
||||
|
||||
game.move.select(Moves.TACKLE);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(typeSpy).toHaveLastReturnedWith(ty);
|
||||
expect(enemySpy).toHaveReturnedWith(1);
|
||||
expect(powerSpy).toHaveReturnedWith(48);
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
});
|
||||
|
||||
// Galvanize specifically would like to check for volt absorb's activation
|
||||
if (ab === Abilities.GALVANIZE) {
|
||||
it("should cause Normal-type attacks to activate Volt Absorb", async () => {
|
||||
game.override.enemyAbility(Abilities.VOLT_ABSORB);
|
||||
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const tySpy = vi.spyOn(playerPokemon, "getMoveType");
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const enemyEffectivenessSpy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
|
||||
|
||||
enemyPokemon.hp = Math.floor(enemyPokemon.getMaxHp() * 0.8);
|
||||
|
||||
game.move.select(Moves.TACKLE);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(tySpy).toHaveLastReturnedWith(PokemonType.ELECTRIC);
|
||||
expect(enemyEffectivenessSpy).toHaveReturnedWith(0);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
});
|
||||
}
|
||||
|
||||
it.each([
|
||||
{ moveName: "Revelation Dance", move: Moves.REVELATION_DANCE, expected_ty: PokemonType.WATER },
|
||||
{ moveName: "Judgement", move: Moves.JUDGMENT, expected_ty: PokemonType.NORMAL },
|
||||
{ moveName: "Terrain Pulse", move: Moves.TERRAIN_PULSE, expected_ty: PokemonType.NORMAL },
|
||||
{ moveName: "Weather Ball", move: Moves.WEATHER_BALL, expected_ty: PokemonType.NORMAL },
|
||||
{ moveName: "Multi Attack", move: Moves.MULTI_ATTACK, expected_ty: PokemonType.NORMAL },
|
||||
{ moveName: "Techno Blast", move: Moves.TECHNO_BLAST, expected_ty: PokemonType.NORMAL },
|
||||
])("should not change the type of $moveName", async ({ move, expected_ty: expectedTy }) => {
|
||||
game.override
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.moveset([move])
|
||||
.starterSpecies(Species.MAGIKARP);
|
||||
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const tySpy = vi.spyOn(playerPokemon, "getMoveType");
|
||||
|
||||
game.move.select(move);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(tySpy).toHaveLastReturnedWith(expectedTy);
|
||||
});
|
||||
|
||||
it("should affect all hits of a Normal-type multi-hit move", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const tySpy = vi.spyOn(playerPokemon, "getMoveType");
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.FURY_SWIPES);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.move.forceHit();
|
||||
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
expect(playerPokemon.turnData.hitCount).toBeGreaterThan(1);
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
|
||||
while (playerPokemon.turnData.hitsLeft > 0) {
|
||||
const enemyStartingHp = enemyPokemon.hp;
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(tySpy).toHaveLastReturnedWith(ty);
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
|
||||
}
|
||||
});
|
||||
|
||||
it("should not be affected by silk scarf after changing the move's type", async () => {
|
||||
game.override.startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.NORMAL }]);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const testMoveInstance = allMoves[Moves.TACKLE];
|
||||
|
||||
// get the power boost from the ability so we can compare it to the item
|
||||
// @ts-expect-error power multiplier is private
|
||||
const boost = allAbilities[ab]?.getAttrs(MoveTypeChangeAbAttr)[0]?.powerMultiplier;
|
||||
expect(boost, "power boost should be defined").toBeDefined();
|
||||
|
||||
const powerSpy = vi.spyOn(testMoveInstance, "calculateBattlePower");
|
||||
const typeSpy = vi.spyOn(game.scene.getPlayerPokemon()!, "getMoveType");
|
||||
game.move.select(Moves.TACKLE);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(typeSpy, "type was not changed").toHaveLastReturnedWith(ty);
|
||||
expect(powerSpy).toHaveLastReturnedWith(toDmgValue(testMoveInstance.power * boost));
|
||||
});
|
||||
|
||||
it("should be affected by the type boosting item after changing the move's type", async () => {
|
||||
game.override.startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: ty }]);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
// get the power boost from the ability so we can compare it to the item
|
||||
// @ts-expect-error power multiplier is private
|
||||
const boost = allAbilities[ab]?.getAttrs(MoveTypeChangeAbAttr)[0]?.powerMultiplier;
|
||||
expect(boost, "power boost should be defined").toBeDefined();
|
||||
|
||||
const tackle = allMoves[Moves.TACKLE];
|
||||
|
||||
const spy = vi.spyOn(tackle, "calculateBattlePower");
|
||||
game.move.select(Moves.TACKLE);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(spy).toHaveLastReturnedWith(toDmgValue(tackle.power * boost * (1 + TYPE_BOOST_ITEM_BOOST_PERCENT / 100)));
|
||||
});
|
||||
});
|
92
test/abilities/normalize.test.ts
Normal file
92
test/abilities/normalize.test.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { TYPE_BOOST_ITEM_BOOST_PERCENT } from "#app/constants";
|
||||
import { allMoves } from "#app/data/moves/move";
|
||||
import { toDmgValue } from "#app/utils/common";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Abilities - Normalize", () => {
|
||||
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
|
||||
.moveset([Moves.TACKLE])
|
||||
.ability(Abilities.NORMALIZE)
|
||||
.battleStyle("single")
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
it("should boost the power of normal type moves by 20%", async () => {
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
const powerSpy = vi.spyOn(allMoves[Moves.TACKLE], "calculateBattlePower");
|
||||
|
||||
game.move.select(Moves.TACKLE);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(powerSpy).toHaveLastReturnedWith(toDmgValue(allMoves[Moves.TACKLE].power * 1.2));
|
||||
});
|
||||
|
||||
it("should not apply the old type boost item after changing a move's type", async () => {
|
||||
game.override.startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.GRASS }]);
|
||||
game.override.moveset([Moves.LEAFAGE]);
|
||||
|
||||
const powerSpy = vi.spyOn(allMoves[Moves.LEAFAGE], "calculateBattlePower");
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
game.move.select(Moves.LEAFAGE);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
// It should return with 1.2 (that is, only the power boost from the ability)
|
||||
expect(powerSpy).toHaveLastReturnedWith(toDmgValue(allMoves[Moves.LEAFAGE].power * 1.2));
|
||||
});
|
||||
|
||||
it("should apply silk scarf's power boost after changing a move's type", async () => {
|
||||
game.override.startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.NORMAL }]);
|
||||
game.override.moveset([Moves.LEAFAGE]);
|
||||
|
||||
const powerSpy = vi.spyOn(allMoves[Moves.LEAFAGE], "calculateBattlePower");
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
game.move.select(Moves.LEAFAGE);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
// 1.2 from normalize boost, second 1.2 from
|
||||
expect(powerSpy).toHaveLastReturnedWith(
|
||||
toDmgValue(allMoves[Moves.LEAFAGE].power * 1.2 * (1 + TYPE_BOOST_ITEM_BOOST_PERCENT / 100)),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ moveName: "Revelation Dance", move: Moves.REVELATION_DANCE },
|
||||
{ moveName: "Judgement", move: Moves.JUDGMENT, expected_ty: PokemonType.NORMAL },
|
||||
{ moveName: "Terrain Pulse", move: Moves.TERRAIN_PULSE },
|
||||
{ moveName: "Weather Ball", move: Moves.WEATHER_BALL },
|
||||
{ moveName: "Multi Attack", move: Moves.MULTI_ATTACK },
|
||||
{ moveName: "Techno Blast", move: Moves.TECHNO_BLAST },
|
||||
{ moveName: "Hidden Power", move: Moves.HIDDEN_POWER },
|
||||
])("should not boost the power of $moveName", async ({ move }) => {
|
||||
game.override.moveset([move]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
const powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower");
|
||||
|
||||
game.move.select(move);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power);
|
||||
});
|
||||
});
|
@ -75,7 +75,7 @@ describe("Moves - Tera Blast", () => {
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(100);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("is super effective against terastallized targets if user is Stellar tera type", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
@ -189,5 +189,33 @@ describe("Moves - Tera Blast", () => {
|
||||
|
||||
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1);
|
||||
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ ab: "galvanize", ty: "electric", ab_id: Abilities.GALVANIZE, ty_id: PokemonType.ELECTRIC },
|
||||
{ ab: "refrigerate", ty: "ice", ab_id: Abilities.REFRIGERATE, ty_id: PokemonType.ICE },
|
||||
{ ab: "pixilate", ty: "fairy", ab_id: Abilities.PIXILATE, ty_id: PokemonType.FAIRY },
|
||||
{ ab: "aerilate", ty: "flying", ab_id: Abilities.AERILATE, ty_id: PokemonType.FLYING },
|
||||
])("should be $ty type if the user has $ab", async ({ ab_id, ty_id }) => {
|
||||
game.override.ability(ab_id).moveset([Moves.TERA_BLAST]).enemyAbility(Abilities.BALL_FETCH);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(playerPokemon.getMoveType(allMoves[Moves.TERA_BLAST])).toBe(ty_id);
|
||||
});
|
||||
|
||||
it("should not be affected by normalize when the user is terastallized with tera normal", async () => {
|
||||
game.override.moveset([Moves.TERA_BLAST]).ability(Abilities.NORMALIZE);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
// override the tera state for the pokemon
|
||||
playerPokemon.isTerastallized = true;
|
||||
playerPokemon.teraType = PokemonType.NORMAL;
|
||||
|
||||
const move = allMoves[Moves.TERA_BLAST];
|
||||
const powerSpy = vi.spyOn(move, "calculateBattlePower");
|
||||
|
||||
game.move.select(Moves.TERA_BLAST);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(powerSpy).toHaveLastReturnedWith(move.power);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user