mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-02-20 03:06:49 +00:00
[Move] Spectral Thief Full Implementation (#4891)
* fully implemented spectral thief * Update to structure of implementation * line commented target.scene.queueMessage since message does not exist yet * changed documentation * added move-trigger.json key * removed line comment since key was added to english locales * removed console.log messages used for debugging * refactored move-trigger key to race with @muscode13 * added more automated tests * github tests failed * removed line comment since key was added to english locales * refactored move-trigger key to race with @muscode13 * added more automated tests * github tests failed * solved conflicts * Update src/data/move.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * removed .partial() * corrected spectral thief name * changed target.scene to globalScene * changed comments --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
This commit is contained in:
parent
5296966f70
commit
b31d5fd23e
@ -4380,6 +4380,69 @@ export class CueNextRoundAttr extends MoveEffectAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute that changes stat stages before the damage is calculated
|
||||
*/
|
||||
export class StatChangeBeforeDmgCalcAttr extends MoveAttr {
|
||||
/**
|
||||
* Applies Stat Changes before damage is calculated
|
||||
*
|
||||
* @param user {@linkcode Pokemon} that called {@linkcode move}
|
||||
* @param target {@linkcode Pokemon} that is the target of {@linkcode move}
|
||||
* @param move {@linkcode Move} called by {@linkcode user}
|
||||
* @param args N/A
|
||||
*
|
||||
* @returns true if stat stages where correctly applied
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Steals the postitive Stat stages of the target before damage calculation so stat changes
|
||||
* apply to damage calculation (e.g. {@linkcode Moves.SPECTRAL_THIEF})
|
||||
* {@link https://bulbapedia.bulbagarden.net/wiki/Spectral_Thief_(move) | Spectral Thief}
|
||||
*/
|
||||
export class SpectralThiefAttr extends StatChangeBeforeDmgCalcAttr {
|
||||
/**
|
||||
* steals max amount of positive stats of the target while not exceeding the limit of max 6 stat stages
|
||||
*
|
||||
* @param user {@linkcode Pokemon} that called {@linkcode move}
|
||||
* @param target {@linkcode Pokemon} that is the target of {@linkcode move}
|
||||
* @param move {@linkcode Move} called by {@linkcode user}
|
||||
* @param args N/A
|
||||
*
|
||||
* @returns true if stat stages where correctly stolen
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
/**
|
||||
* Copy all positive stat stages to user and reduce copied stat stages on target.
|
||||
*/
|
||||
for (const s of BATTLE_STATS) {
|
||||
const statStageValueTarget = target.getStatStage(s);
|
||||
const statStageValueUser = user.getStatStage(s);
|
||||
|
||||
if (statStageValueTarget > 0) {
|
||||
/**
|
||||
* Only value of up to 6 can be stolen (stat stages don't exceed 6)
|
||||
*/
|
||||
const availableToSteal = Math.min(statStageValueTarget, 6 - statStageValueUser);
|
||||
|
||||
globalScene.unshiftPhase(new StatStageChangePhase(user.getBattlerIndex(), this.selfTarget, [ s ], availableToSteal));
|
||||
target.setStatStage(s, statStageValueTarget - availableToSteal);
|
||||
}
|
||||
}
|
||||
|
||||
target.updateInfo();
|
||||
user.updateInfo();
|
||||
globalScene.queueMessage(i18next.t("moveTriggers:stealPositiveStats", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class VariableAtkAttr extends MoveAttr {
|
||||
constructor() {
|
||||
super();
|
||||
@ -10467,8 +10530,8 @@ export function initMoves() {
|
||||
new AttackMove(Moves.PRISMATIC_LASER, Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, 0, 7)
|
||||
.attr(RechargeAttr),
|
||||
new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7)
|
||||
.ignoresSubstitute()
|
||||
.partial(), // Does not steal stats
|
||||
.attr(SpectralThiefAttr)
|
||||
.ignoresSubstitute(),
|
||||
new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7)
|
||||
.ignoresAbilities(),
|
||||
new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7)
|
||||
|
@ -7,7 +7,40 @@ import { variantColorCache } from "#app/data/variant";
|
||||
import { variantData } from "#app/data/variant";
|
||||
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info";
|
||||
import type Move from "#app/data/move";
|
||||
import { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr, VariableMoveTypeChartAttr, HpSplitAttr } from "#app/data/move";
|
||||
import {
|
||||
HighCritAttr,
|
||||
StatChangeBeforeDmgCalcAttr,
|
||||
HitsTagAttr,
|
||||
applyMoveAttrs,
|
||||
FixedDamageAttr,
|
||||
VariableAtkAttr,
|
||||
allMoves,
|
||||
MoveCategory,
|
||||
TypelessAttr,
|
||||
CritOnlyAttr,
|
||||
getMoveTargets,
|
||||
OneHitKOAttr,
|
||||
VariableMoveTypeAttr,
|
||||
VariableDefAttr,
|
||||
AttackMove,
|
||||
ModifiedDamageAttr,
|
||||
VariableMoveTypeMultiplierAttr,
|
||||
IgnoreOpponentStatStagesAttr,
|
||||
SacrificialAttr,
|
||||
VariableMoveCategoryAttr,
|
||||
CounterDamageAttr,
|
||||
StatStageChangeAttr,
|
||||
RechargeAttr,
|
||||
IgnoreWeatherTypeDebuffAttr,
|
||||
BypassBurnDamageReductionAttr,
|
||||
SacrificialAttrOnHit,
|
||||
OneHitKOAccuracyAttr,
|
||||
RespectAttackTypeImmunityAttr,
|
||||
MoveTarget,
|
||||
CombinedPledgeStabBoostAttr,
|
||||
VariableMoveTypeChartAttr,
|
||||
HpSplitAttr
|
||||
} from "#app/data/move";
|
||||
import type { PokemonSpeciesForm } from "#app/data/pokemon-species";
|
||||
import { default as PokemonSpecies, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species";
|
||||
import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
|
||||
@ -2903,6 +2936,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
isCritical = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies stat changes from {@linkcode move} and gives it to {@linkcode source}
|
||||
* before damage calculation
|
||||
*/
|
||||
applyMoveAttrs(StatChangeBeforeDmgCalcAttr, source, this, move);
|
||||
|
||||
const { cancelled, result, damage: dmg } = this.getAttackDamage(source, move, false, false, isCritical, false);
|
||||
|
||||
const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === source.getMoveType(move)) as TypeBoostTag;
|
||||
|
224
src/test/moves/spectral_thief.test.ts
Normal file
224
src/test/moves/spectral_thief.test.ts
Normal file
@ -0,0 +1,224 @@
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { allMoves } from "#app/data/move";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { TurnEndPhase } from "#app/phases/turn-end-phase";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Spectral Thief", () => {
|
||||
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
|
||||
.enemySpecies(Species.SHUCKLE)
|
||||
.enemyLevel(100)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.moveset([ Moves.SPECTRAL_THIEF, Moves.SPLASH ])
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.disableCrits;
|
||||
});
|
||||
|
||||
it("should steal max possible positive stat changes and ignore negative ones.", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
enemy.setStatStage(Stat.ATK, 6);
|
||||
enemy.setStatStage(Stat.DEF, -6);
|
||||
enemy.setStatStage(Stat.SPATK, 6);
|
||||
enemy.setStatStage(Stat.SPDEF, -6);
|
||||
enemy.setStatStage(Stat.SPD, 3);
|
||||
|
||||
player.setStatStage(Stat.ATK, 4);
|
||||
player.setStatStage(Stat.DEF, 1);
|
||||
player.setStatStage(Stat.SPATK, 0);
|
||||
player.setStatStage(Stat.SPDEF, 0);
|
||||
player.setStatStage(Stat.SPD, -2);
|
||||
|
||||
game.move.select(Moves.SPECTRAL_THIEF);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
/**
|
||||
* enemy has +6 ATK and player +4 => player only steals +2
|
||||
* enemy has -6 DEF and player 1 => player should not steal
|
||||
* enemy has +6 SPATK and player 0 => player only steals +6
|
||||
* enemy has -6 SPDEF and player 0 => player should not steal
|
||||
* enemy has +3 SPD and player -2 => player only steals +3
|
||||
*/
|
||||
expect(player.getStatStages()).toEqual([ 6, 1, 6, 0, 1, 0, 0 ]);
|
||||
expect(enemy.getStatStages()).toEqual([ 4, -6, 0, -6, 0, 0, 0 ]);
|
||||
});
|
||||
|
||||
it("should steal stat stages before dmg calculation", async () => {
|
||||
game.override
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyLevel(50);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
const moveToCheck = allMoves[Moves.SPECTRAL_THIEF];
|
||||
const dmgBefore = enemy.getAttackDamage(player, moveToCheck, false, false, false, false).damage;
|
||||
|
||||
enemy.setStatStage(Stat.ATK, 6);
|
||||
|
||||
player.setStatStage(Stat.ATK, 0);
|
||||
|
||||
game.move.select(Moves.SPECTRAL_THIEF);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(dmgBefore).toBeLessThan(enemy.getAttackDamage(player, moveToCheck, false, false, false, false).damage);
|
||||
});
|
||||
|
||||
it("should steal stat stages as a negative value with Contrary.", async () => {
|
||||
game.override
|
||||
.ability(Abilities.CONTRARY);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
enemy.setStatStage(Stat.ATK, 6);
|
||||
|
||||
player.setStatStage(Stat.ATK, 0);
|
||||
|
||||
game.move.select(Moves.SPECTRAL_THIEF);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(player.getStatStage(Stat.ATK)).toEqual(-6);
|
||||
expect(enemy.getStatStage(Stat.ATK)).toEqual(0);
|
||||
});
|
||||
|
||||
it("should steal double the stat stages with Simple.", async () => {
|
||||
game.override
|
||||
.ability(Abilities.SIMPLE);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
enemy.setStatStage(Stat.ATK, 3);
|
||||
|
||||
player.setStatStage(Stat.ATK, 0);
|
||||
|
||||
game.move.select(Moves.SPECTRAL_THIEF);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(player.getStatStage(Stat.ATK)).toEqual(6);
|
||||
expect(enemy.getStatStage(Stat.ATK)).toEqual(0);
|
||||
});
|
||||
|
||||
it("should steal the stat stages through Clear Body.", async () => {
|
||||
game.override
|
||||
.enemyAbility(Abilities.CLEAR_BODY);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
enemy.setStatStage(Stat.ATK, 3);
|
||||
|
||||
player.setStatStage(Stat.ATK, 0);
|
||||
|
||||
game.move.select(Moves.SPECTRAL_THIEF);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(player.getStatStage(Stat.ATK)).toEqual(3);
|
||||
expect(enemy.getStatStage(Stat.ATK)).toEqual(0);
|
||||
});
|
||||
|
||||
it("should steal the stat stages through White Smoke.", async () => {
|
||||
game.override
|
||||
.enemyAbility(Abilities.WHITE_SMOKE);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
enemy.setStatStage(Stat.ATK, 3);
|
||||
|
||||
player.setStatStage(Stat.ATK, 0);
|
||||
|
||||
game.move.select(Moves.SPECTRAL_THIEF);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(player.getStatStage(Stat.ATK)).toEqual(3);
|
||||
expect(enemy.getStatStage(Stat.ATK)).toEqual(0);
|
||||
});
|
||||
|
||||
it("should steal the stat stages through Hyper Cutter.", async () => {
|
||||
game.override
|
||||
.enemyAbility(Abilities.HYPER_CUTTER);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
enemy.setStatStage(Stat.ATK, 3);
|
||||
|
||||
player.setStatStage(Stat.ATK, 0);
|
||||
|
||||
game.move.select(Moves.SPECTRAL_THIEF);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(player.getStatStage(Stat.ATK)).toEqual(3);
|
||||
expect(enemy.getStatStage(Stat.ATK)).toEqual(0);
|
||||
});
|
||||
|
||||
it("should bypass Substitute.", async () => {
|
||||
game.override
|
||||
.enemyMoveset(Moves.SUBSTITUTE);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
enemy.setStatStage(Stat.ATK, 3);
|
||||
|
||||
player.setStatStage(Stat.ATK, 0);
|
||||
|
||||
game.move.select(Moves.SPECTRAL_THIEF);
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(player.getStatStage(Stat.ATK)).toEqual(3);
|
||||
expect(enemy.getStatStage(Stat.ATK)).toEqual(0);
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp() - 1);
|
||||
});
|
||||
|
||||
it("should get blocked by protect.", async () => {
|
||||
game.override
|
||||
.enemyMoveset(Moves.PROTECT);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
enemy.setStatStage(Stat.ATK, 3);
|
||||
|
||||
player.setStatStage(Stat.ATK, 0);
|
||||
|
||||
game.move.select(Moves.SPECTRAL_THIEF);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(player.getStatStage(Stat.ATK)).toEqual(0);
|
||||
expect(enemy.getStatStage(Stat.ATK)).toEqual(3);
|
||||
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user