[Move] Triple Arrows effect chance for stat change is now 50% (#4543)

* Triple Arrows effect chance for stat change is now properly 50%

* Add tsdocs to `StatStageChangeAttr`

* Add test for Serene Grace interaction

* Fix linting

---------

Co-authored-by: Mumble <171087428+frutescens@users.noreply.github.com>
This commit is contained in:
NightKev 2024-10-08 05:32:51 -07:00 committed by GitHub
parent f5fa478eb8
commit a1ca7e632b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 87 additions and 22 deletions

View File

@ -970,13 +970,16 @@ export class MoveEffectAttr extends MoveAttr {
public lastHitOnly: boolean; public lastHitOnly: boolean;
/** Should this effect only apply on the first target hit? */ /** Should this effect only apply on the first target hit? */
public firstTargetOnly: boolean; public firstTargetOnly: boolean;
/** Overrides the secondary effect chance for this attr if set. */
public effectChanceOverride?: number;
constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false, lastHitOnly: boolean = false, firstTargetOnly: boolean = false) { constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false, lastHitOnly: boolean = false, firstTargetOnly: boolean = false, effectChanceOverride?: number) {
super(selfTarget); super(selfTarget);
this.trigger = trigger !== undefined ? trigger : MoveEffectTrigger.POST_APPLY; this.trigger = trigger ?? MoveEffectTrigger.POST_APPLY;
this.firstHitOnly = firstHitOnly; this.firstHitOnly = firstHitOnly;
this.lastHitOnly = lastHitOnly; this.lastHitOnly = lastHitOnly;
this.firstTargetOnly = firstTargetOnly; this.firstTargetOnly = firstTargetOnly;
this.effectChanceOverride = effectChanceOverride;
} }
/** /**
@ -1001,15 +1004,15 @@ export class MoveEffectAttr extends MoveAttr {
/** /**
* Gets the used move's additional effect chance. * Gets the used move's additional effect chance.
* If user's ability has MoveEffectChanceMultiplierAbAttr or IgnoreMoveEffectsAbAttr modifies the base chance. * Chance is modified by {@linkcode MoveEffectChanceMultiplierAbAttr} and {@linkcode IgnoreMoveEffectsAbAttr}.
* @param user {@linkcode Pokemon} using this move * @param user {@linkcode Pokemon} using this move
* @param target {@linkcode Pokemon} target of this move * @param target {@linkcode Pokemon | Target} of this move
* @param move {@linkcode Move} being used * @param move {@linkcode Move} being used
* @param selfEffect {@linkcode Boolean} if move targets user. * @param selfEffect `true` if move targets user.
* @returns Move chance value. * @returns Move effect chance value.
*/ */
getMoveChance(user: Pokemon, target: Pokemon, move: Move, selfEffect?: Boolean, showAbility?: Boolean): integer { getMoveChance(user: Pokemon, target: Pokemon, move: Move, selfEffect?: Boolean, showAbility?: Boolean): integer {
const moveChance = new Utils.NumberHolder(move.chance); const moveChance = new Utils.NumberHolder(this.effectChanceOverride ?? move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, moveChance, move, target, selfEffect, showAbility); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, moveChance, move, target, selfEffect, showAbility);
@ -2752,14 +2755,17 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
/** /**
* Attribute used for moves that change stat stages * 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 stats {@linkcode BattleStat} Array of stat(s) to change
* @param selfTarget whether the changes are applied to the user (true) or the target (false) * @param stages How many stages to change the stat(s) by, [-6, 6]
* @param condition {@linkcode MoveConditionFunc} optional condition to trigger the stat change * @param selfTarget `true` if the move is self-targetting
* @param firstHitOnly whether the stat change only applies on the first hit of a multi hit move * @param condition {@linkcode MoveConditionFunc} Optional condition to be checked in order to apply the changes
* @param moveEffectTrigger {@linkcode MoveEffectTrigger} the trigger for the effect to take place * @param showMessage `true` to display a message; default `true`
* @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 firstHitOnly `true` if only the first hit of a multi hit move should cause a stat stage change; default `false`
* @param lastHitOnly whether the effect should only apply after the last hit of a multi hit move * @param moveEffectTrigger {@linkcode MoveEffectTrigger} When the stat change should trigger; default {@linkcode MoveEffectTrigger.HIT}
* @param firstTargetOnly `true` if a move that hits multiple pokemon should only trigger the stat change if it hits at least one pokemon, rather than once per hit pokemon; default `false`
* @param lastHitOnly `true` if the effect should only apply after the last hit of a multi hit move; default `false`
* @param effectChanceOverride Will override the move's normal secondary effect chance if specified
* *
* @extends MoveEffectAttr * @extends MoveEffectAttr
* @see {@linkcode apply} * @see {@linkcode apply}
@ -2767,14 +2773,14 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
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, lastHitOnly: 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, effectChanceOverride?: number) {
super(selfTarget, moveEffectTrigger, firstHitOnly, lastHitOnly, firstTargetOnly); super(selfTarget, moveEffectTrigger, firstHitOnly, lastHitOnly, firstTargetOnly, effectChanceOverride);
this.stats = stats; this.stats = stats;
this.stages = stages; this.stages = stages;
this.condition = condition!; // TODO: is this bang correct? this.condition = condition;
this.showMessage = showMessage; this.showMessage = showMessage;
} }
@ -9556,9 +9562,8 @@ export function initMoves() {
new AttackMove(Moves.TRIPLE_ARROWS, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8) new AttackMove(Moves.TRIPLE_ARROWS, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8)
.makesContact(false) .makesContact(false)
.attr(HighCritAttr) .attr(HighCritAttr)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1) .attr(StatStageChangeAttr, [ Stat.DEF ], -1, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 50)
.attr(FlinchAttr) .attr(FlinchAttr),
.partial(),
new AttackMove(Moves.INFERNAL_PARADE, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 15, 30, 0, 8) new AttackMove(Moves.INFERNAL_PARADE, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 15, 30, 0, 8)
.attr(StatusEffectAttr, StatusEffect.BURN) .attr(StatusEffectAttr, StatusEffect.BURN)
.attr(MovePowerMultiplierAttr, (user, target, move) => target.status ? 2 : 1), .attr(MovePowerMultiplierAttr, (user, target, move) => target.status ? 2 : 1),

View File

@ -0,0 +1,60 @@
import { allMoves, FlinchAttr, StatStageChangeAttr } from "#app/data/move";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Moves - Triple Arrows", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const tripleArrows = allMoves[Moves.TRIPLE_ARROWS];
const flinchAttr = tripleArrows.getAttrs(FlinchAttr)[0];
const defDropAttr = tripleArrows.getAttrs(StatStageChangeAttr)[0];
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.ability(Abilities.BALL_FETCH)
.moveset([ Moves.TRIPLE_ARROWS ])
.battleType("single")
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.STURDY)
.enemyMoveset(Moves.SPLASH);
vi.spyOn(flinchAttr, "getMoveChance");
vi.spyOn(defDropAttr, "getMoveChance");
});
it("has a 30% flinch chance and 50% defense drop chance", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.TRIPLE_ARROWS);
await game.phaseInterceptor.to("BerryPhase");
expect(flinchAttr.getMoveChance).toHaveReturnedWith(30);
expect(defDropAttr.getMoveChance).toHaveReturnedWith(50);
});
it("is affected normally by Serene Grace", async () => {
game.override.ability(Abilities.SERENE_GRACE);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.TRIPLE_ARROWS);
await game.phaseInterceptor.to("BerryPhase");
expect(flinchAttr.getMoveChance).toHaveReturnedWith(60);
expect(defDropAttr.getMoveChance).toHaveReturnedWith(100);
});
});