mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-05-09 09:54:37 +01: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 {
|
export class VariableAtkAttr extends MoveAttr {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -10467,8 +10530,8 @@ export function initMoves() {
|
|||||||
new AttackMove(Moves.PRISMATIC_LASER, Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, 0, 7)
|
new AttackMove(Moves.PRISMATIC_LASER, Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, 0, 7)
|
||||||
.attr(RechargeAttr),
|
.attr(RechargeAttr),
|
||||||
new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7)
|
new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7)
|
||||||
.ignoresSubstitute()
|
.attr(SpectralThiefAttr)
|
||||||
.partial(), // Does not steal stats
|
.ignoresSubstitute(),
|
||||||
new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7)
|
new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7)
|
||||||
.ignoresAbilities(),
|
.ignoresAbilities(),
|
||||||
new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7)
|
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 { variantData } from "#app/data/variant";
|
||||||
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info";
|
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info";
|
||||||
import type Move from "#app/data/move";
|
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 type { PokemonSpeciesForm } from "#app/data/pokemon-species";
|
||||||
import { default as PokemonSpecies, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } 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";
|
import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
|
||||||
@ -2903,6 +2936,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
isCritical = false;
|
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 { 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;
|
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