[Move] Implement Scale Shot (#4551)

* Scale Shot

* Docstrings for StatStageChangeAttr

* Add test for scale shot
This commit is contained in:
AJ Fontaine 2024-10-03 11:17:51 -04:00 committed by GitHub
parent 8fc0d9a429
commit ea9e0c7909
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 101 additions and 6 deletions

View File

@ -2668,20 +2668,42 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
} }
} }
/**
* Attribute used for moves that change stat stages
* @param stats {@linkcode BattleStat} array of stats to be changed
* @param stages stages by which to change the stats, from -6 to 6
* @param selfTarget whether the changes are applied to the user (true) or the target (false)
* @param condition {@linkcode MoveConditionFunc} optional condition to trigger the stat change
* @param firstHitOnly whether the stat change only applies on the first hit of a multi hit move
* @param moveEffectTrigger {@linkcode MoveEffectTrigger} the trigger for the effect to take place
* @param firstTargetOnly whether, if this is a multi target move, to only apply the effect after the first target is hit, rather than once for each target
* @param lastHitOnly whether the effect should only apply after the last hit of a multi hit move
*
* @extends MoveEffectAttr
* @see {@linkcode apply}
*/
export class StatStageChangeAttr extends MoveEffectAttr { export class StatStageChangeAttr extends MoveEffectAttr {
public stats: BattleStat[]; public stats: BattleStat[];
public stages: integer; public stages: integer;
private condition: MoveConditionFunc | null; private condition: MoveConditionFunc | null;
private showMessage: boolean; private showMessage: boolean;
constructor(stats: BattleStat[], stages: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false) { constructor(stats: BattleStat[], stages: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false, lastHitOnly: boolean = false) {
super(selfTarget, moveEffectTrigger, firstHitOnly, false, firstTargetOnly); super(selfTarget, moveEffectTrigger, firstHitOnly, lastHitOnly, firstTargetOnly);
this.stats = stats; this.stats = stats;
this.stages = stages; this.stages = stages;
this.condition = condition!; // TODO: is this bang correct? this.condition = condition!; // TODO: is this bang correct?
this.showMessage = showMessage; this.showMessage = showMessage;
} }
/**
* Attempts to change stats of the user or target (depending on value of selfTarget) if conditions are met
* @param user {@linkcode Pokemon} the user of the move
* @param target {@linkcode Pokemon} the target of the move
* @param move {@linkcode Move} the move
* @param args unused
* @returns whether stat stages were changed
*/
apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean | Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean | Promise<boolean> {
if (!super.apply(user, target, move, args) || (this.condition && !this.condition(user, target, move))) { if (!super.apply(user, target, move, args) || (this.condition && !this.condition(user, target, move))) {
return false; return false;
@ -9154,11 +9176,10 @@ export function initMoves() {
.attr(ClearTerrainAttr) .attr(ClearTerrainAttr)
.condition((user, target, move) => !!user.scene.arena.terrain), .condition((user, target, move) => !!user.scene.arena.terrain),
new AttackMove(Moves.SCALE_SHOT, Type.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8) new AttackMove(Moves.SCALE_SHOT, Type.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8)
//.attr(StatStageChangeAttr, Stat.SPD, 1, true) // TODO: Have boosts only apply at end of move, not after every hit .attr(StatStageChangeAttr, [Stat.SPD], 1, true, null, true, false, MoveEffectTrigger.HIT, false, true)
//.attr(StatStageChangeAttr, Stat.DEF, -1, true) .attr(StatStageChangeAttr, [Stat.DEF], -1, true, null, true, false, MoveEffectTrigger.HIT, false, true)
.attr(MultiHitAttr) .attr(MultiHitAttr)
.makesContact(false) .makesContact(false),
.partial(),
new AttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, 100, 0, 8) new AttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, 100, 0, 8)
.attr(ChargeAttr, ChargeAnim.METEOR_BEAM_CHARGING, i18next.t("moveTriggers:isOverflowingWithSpacePower", {pokemonName: "{USER}"}), null, true) .attr(ChargeAttr, ChargeAnim.METEOR_BEAM_CHARGING, i18next.t("moveTriggers:isOverflowingWithSpacePower", {pokemonName: "{USER}"}), null, true)
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)

View File

@ -0,0 +1,74 @@
import { DamagePhase } from "#app/phases/damage-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest";
describe("Moves - Scale Shot", () => {
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.SCALE_SHOT])
.battleType("single")
.disableCrits()
.starterSpecies(Species.MINCCINO)
.ability(Abilities.NO_GUARD)
.passiveAbility(Abilities.SKILL_LINK)
.enemyAbility(Abilities.SHEER_FORCE)
.enemyPassiveAbility(Abilities.STALL)
.enemyMoveset(Moves.SKILL_SWAP)
.enemyLevel(5);
});
it("applies stat changes after last hit", async () => {
await game.classicMode.startBattle([Species.FORRETRESS]);
const minccino = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SCALE_SHOT);
await game.phaseInterceptor.to(MoveEffectPhase);
await game.phaseInterceptor.to(DamagePhase);
await game.phaseInterceptor.to(MoveEffectPhase);
expect (minccino?.getStatStage(Stat.DEF)).toBe(0);
expect (minccino?.getStatStage(Stat.SPD)).toBe(0);
await game.phaseInterceptor.to(MoveEndPhase);
expect (minccino.getStatStage(Stat.DEF)).toBe(-1);
expect (minccino.getStatStage(Stat.SPD)).toBe(1);
});
it("unaffected by sheer force", async () => {
await game.classicMode.startBattle([Species.WOBBUFFET]);
const minccino = game.scene.getPlayerPokemon()!;
const wobbuffet = game.scene.getEnemyPokemon()!;
wobbuffet.setStat(Stat.HP, 100, true);
wobbuffet.hp = 100;
game.move.select(Moves.SCALE_SHOT);
await game.phaseInterceptor.to(TurnEndPhase);
const hpafter1 = wobbuffet.hp;
//effect not nullified by sheer force
expect (minccino.getStatStage(Stat.DEF)).toBe(-1);
expect (minccino.getStatStage(Stat.SPD)).toBe(1);
game.move.select(Moves.SCALE_SHOT);
await game.phaseInterceptor.to(MoveEndPhase);
const hpafter2 = wobbuffet.hp;
//check damage not boosted- make damage before sheer force a little lower than theoretical boosted sheer force damage
expect (100 - hpafter1).toBe(hpafter1 - hpafter2);
});
});