[Move] Finish Alluring Voice, Burning Jealousy and Lash Out (#3508)
* Implement Alluring Voice and Burning Jealousy * Fix Alluring Voice and add tests * Replace `BattlerTag.STATS_BOOSTED` with `PokemonTurnData` field * Work around bug with turn data * Remove unused variable * Replace nearby instances of `integer` with `number` * Fix imports * Implement Lash Out * Rename `battleStats(In|De)crease` -> `battleStats(In|De)creased` * Fix copy/paste error Co-authored-by: schmidtc1 <62030095+schmidtc1@users.noreply.github.com> * Update tests --------- Co-authored-by: ElliottSimmonds <simmonds.elliott@yahoo.co.uk> Co-authored-by: schmidtc1 <62030095+schmidtc1@users.noreply.github.com> Co-authored-by: Mumble <171087428+frutescens@users.noreply.github.com>
This commit is contained in:
parent
dcb03f4ee9
commit
709066bd1a
|
@ -6037,6 +6037,57 @@ export class DestinyBondAttr extends MoveEffectAttr {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to apply a battler tag to the target if they have had their stats boosted this turn.
|
||||
* @extends AddBattlerTagAttr
|
||||
*/
|
||||
export class AddBattlerTagIfBoostedAttr extends AddBattlerTagAttr {
|
||||
constructor(tag: BattlerTagType) {
|
||||
super(tag, false, false, 2, 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param user {@linkcode Pokemon} using this move
|
||||
* @param target {@linkcode Pokemon} target of this move
|
||||
* @param move {@linkcode Move} being used
|
||||
* @param {any[]} args N/A
|
||||
* @returns true
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (target.turnData.battleStatsIncreased) {
|
||||
super.apply(user, target, move, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to apply a status effect to the target if they have had their stats boosted this turn.
|
||||
* @extends MoveEffectAttr
|
||||
*/
|
||||
export class StatusIfBoostedAttr extends MoveEffectAttr {
|
||||
public effect: StatusEffect;
|
||||
|
||||
constructor(effect: StatusEffect) {
|
||||
super(true, MoveEffectTrigger.HIT);
|
||||
this.effect = effect;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param user {@linkcode Pokemon} using this move
|
||||
* @param target {@linkcode Pokemon} target of this move
|
||||
* @param move {@linkcode Move} N/A
|
||||
* @param {any[]} args N/A
|
||||
* @returns true
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (target.turnData.battleStatsIncreased) {
|
||||
target.trySetStatus(this.effect, true, user);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class LastResortAttr extends MoveAttr {
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user: Pokemon, target: Pokemon, move: Move) => {
|
||||
|
@ -8694,10 +8745,10 @@ export function initMoves() {
|
|||
new AttackMove(Moves.SKITTER_SMACK, Type.BUG, MoveCategory.PHYSICAL, 70, 90, 10, 100, 0, 8)
|
||||
.attr(StatChangeAttr, BattleStat.SPATK, -1),
|
||||
new AttackMove(Moves.BURNING_JEALOUSY, Type.FIRE, MoveCategory.SPECIAL, 70, 100, 5, 100, 0, 8)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES)
|
||||
.partial(),
|
||||
.attr(StatusIfBoostedAttr, StatusEffect.BURN)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new AttackMove(Moves.LASH_OUT, Type.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8)
|
||||
.partial(),
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => user.turnData.battleStatsDecreased ? 2 : 1),
|
||||
new AttackMove(Moves.POLTERGEIST, Type.GHOST, MoveCategory.PHYSICAL, 110, 90, 5, -1, 0, 8)
|
||||
.attr(AttackedByItemAttr)
|
||||
.makesContact(false),
|
||||
|
@ -9146,8 +9197,8 @@ export function initMoves() {
|
|||
.attr(AddBattlerTagAttr, BattlerTagType.DRAGON_CHEER, false, true)
|
||||
.target(MoveTarget.NEAR_ALLY),
|
||||
new AttackMove(Moves.ALLURING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 9)
|
||||
.soundBased()
|
||||
.partial(),
|
||||
.attr(AddBattlerTagIfBoostedAttr, BattlerTagType.CONFUSED)
|
||||
.soundBased(),
|
||||
new AttackMove(Moves.TEMPER_FLARE, Type.FIRE, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 9)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => user.getLastXMoves(2)[1]?.result === MoveResult.MISS || user.getLastXMoves(2)[1]?.result === MoveResult.FAIL ? 2 : 1),
|
||||
new AttackMove(Moves.SUPERCELL_SLAM, Type.ELECTRIC, MoveCategory.PHYSICAL, 100, 95, 15, -1, 0, 9)
|
||||
|
|
|
@ -4286,7 +4286,7 @@ export interface TurnMove {
|
|||
targets?: BattlerIndex[];
|
||||
result: MoveResult;
|
||||
virtual?: boolean;
|
||||
turn?: integer;
|
||||
turn?: number;
|
||||
}
|
||||
|
||||
export interface QueuedMove {
|
||||
|
@ -4298,17 +4298,17 @@ export interface QueuedMove {
|
|||
export interface AttackMoveResult {
|
||||
move: Moves;
|
||||
result: DamageResult;
|
||||
damage: integer;
|
||||
damage: number;
|
||||
critical: boolean;
|
||||
sourceId: integer;
|
||||
sourceId: number;
|
||||
sourceBattlerIndex: BattlerIndex;
|
||||
}
|
||||
|
||||
export class PokemonSummonData {
|
||||
public battleStats: integer[] = [ 0, 0, 0, 0, 0, 0, 0 ];
|
||||
public battleStats: number[] = [ 0, 0, 0, 0, 0, 0, 0 ];
|
||||
public moveQueue: QueuedMove[] = [];
|
||||
public disabledMove: Moves = Moves.NONE;
|
||||
public disabledTurns: integer = 0;
|
||||
public disabledTurns: number = 0;
|
||||
public tags: BattlerTag[] = [];
|
||||
public abilitySuppressed: boolean = false;
|
||||
public abilitiesApplied: Abilities[] = [];
|
||||
|
@ -4318,14 +4318,14 @@ export class PokemonSummonData {
|
|||
public ability: Abilities = Abilities.NONE;
|
||||
public gender: Gender;
|
||||
public fusionGender: Gender;
|
||||
public stats: integer[];
|
||||
public stats: number[];
|
||||
public moveset: (PokemonMove | null)[];
|
||||
// If not initialized this value will not be populated from save data.
|
||||
public types: Type[] = [];
|
||||
}
|
||||
|
||||
export class PokemonBattleData {
|
||||
public hitCount: integer = 0;
|
||||
public hitCount: number = 0;
|
||||
public endured: boolean = false;
|
||||
public berriesEaten: BerryType[] = [];
|
||||
public abilitiesApplied: Abilities[] = [];
|
||||
|
@ -4334,21 +4334,23 @@ export class PokemonBattleData {
|
|||
|
||||
export class PokemonBattleSummonData {
|
||||
/** The number of turns the pokemon has passed since entering the battle */
|
||||
public turnCount: integer = 1;
|
||||
public turnCount: number = 1;
|
||||
/** The list of moves the pokemon has used since entering the battle */
|
||||
public moveHistory: TurnMove[] = [];
|
||||
}
|
||||
|
||||
export class PokemonTurnData {
|
||||
public flinched: boolean;
|
||||
public acted: boolean;
|
||||
public hitCount: integer;
|
||||
public hitsLeft: integer;
|
||||
public damageDealt: integer = 0;
|
||||
public currDamageDealt: integer = 0;
|
||||
public damageTaken: integer = 0;
|
||||
public flinched: boolean = false;
|
||||
public acted: boolean = false;
|
||||
public hitCount: number;
|
||||
public hitsLeft: number;
|
||||
public damageDealt: number = 0;
|
||||
public currDamageDealt: number = 0;
|
||||
public damageTaken: number = 0;
|
||||
public attacksReceived: AttackMoveResult[] = [];
|
||||
public order: number;
|
||||
public battleStatsIncreased: boolean = false;
|
||||
public battleStatsDecreased: boolean = false;
|
||||
}
|
||||
|
||||
export enum AiType {
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import BattleScene from "#app/battle-scene.js";
|
||||
import { BattlerIndex } from "#app/battle.js";
|
||||
import { applyPreStatChangeAbAttrs, ProtectStatAbAttr, applyAbAttrs, StatChangeMultiplierAbAttr, StatChangeCopyAbAttr, applyPostStatChangeAbAttrs, PostStatChangeAbAttr } from "#app/data/ability.js";
|
||||
import { MistTag, ArenaTagSide } from "#app/data/arena-tag.js";
|
||||
import { BattleStat, getBattleStatName, getBattleStatLevelChangeDescription } from "#app/data/battle-stat.js";
|
||||
import Pokemon from "#app/field/pokemon.js";
|
||||
import { getPokemonNameWithAffix } from "#app/messages.js";
|
||||
import { PokemonResetNegativeStatStageModifier } from "#app/modifier/modifier.js";
|
||||
import { handleTutorial, Tutorial } from "#app/tutorial.js";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { applyAbAttrs, applyPostStatChangeAbAttrs, applyPreStatChangeAbAttrs, PostStatChangeAbAttr, ProtectStatAbAttr, StatChangeCopyAbAttr, StatChangeMultiplierAbAttr } from "#app/data/ability";
|
||||
import { ArenaTagSide, MistTag } from "#app/data/arena-tag";
|
||||
import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "#app/data/battle-stat";
|
||||
import Pokemon from "#app/field/pokemon";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { PokemonResetNegativeStatStageModifier } from "#app/modifier/modifier";
|
||||
import { handleTutorial, Tutorial } from "#app/tutorial";
|
||||
import * as Utils from "#app/utils";
|
||||
import i18next from "i18next";
|
||||
import * as Utils from "#app/utils.js";
|
||||
import { PokemonPhase } from "./pokemon-phase";
|
||||
|
||||
export type StatChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void;
|
||||
|
@ -72,7 +72,7 @@ export class StatChangePhase extends PokemonPhase {
|
|||
}
|
||||
|
||||
const battleStats = this.getPokemon().summonData.battleStats;
|
||||
const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats![stat] + levels.value, 6) : Math.max(battleStats![stat] + levels.value, -6)) - battleStats![stat]);
|
||||
const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats[stat] + levels.value, 6) : Math.max(battleStats[stat] + levels.value, -6)) - battleStats[stat]);
|
||||
|
||||
this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels);
|
||||
|
||||
|
@ -85,6 +85,20 @@ export class StatChangePhase extends PokemonPhase {
|
|||
}
|
||||
|
||||
for (const stat of filteredStats) {
|
||||
if (levels.value > 0 && pokemon.summonData.battleStats[stat] < 6) {
|
||||
if (!pokemon.turnData) {
|
||||
// Temporary fix for missing turn data struct on turn 1
|
||||
pokemon.resetTurnData();
|
||||
}
|
||||
pokemon.turnData.battleStatsIncreased = true;
|
||||
} else if (levels.value < 0 && pokemon.summonData.battleStats[stat] > -6) {
|
||||
if (!pokemon.turnData) {
|
||||
// Temporary fix for missing turn data struct on turn 1
|
||||
pokemon.resetTurnData();
|
||||
}
|
||||
pokemon.turnData.battleStatsDecreased = true;
|
||||
}
|
||||
|
||||
pokemon.summonData.battleStats[stat] = Math.max(Math.min(pokemon.summonData.battleStats[stat] + levels.value, 6), -6);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { BattlerIndex } from "#app/battle";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import { BerryPhase } from "#app/phases/berry-phase";
|
||||
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 } from "vitest";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
describe("Moves - Alluring Voice", () => {
|
||||
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
|
||||
.battleType("single")
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.ICE_SCALES)
|
||||
.enemyMoveset(Array(4).fill(Moves.HOWL))
|
||||
.startingLevel(10)
|
||||
.enemyLevel(10)
|
||||
.starterSpecies(Species.FEEBAS)
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.moveset([Moves.ALLURING_VOICE]);
|
||||
|
||||
});
|
||||
|
||||
it("should confuse the opponent if their stats were raised", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.ALLURING_VOICE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to(BerryPhase);
|
||||
|
||||
expect(enemy.getTag(BattlerTagType.CONFUSED)?.tagType).toBe("CONFUSED");
|
||||
}, TIMEOUT);
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
import { BattlerIndex } from "#app/battle";
|
||||
import { allMoves } from "#app/data/move";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { StatusEffect } from "#app/enums/status-effect";
|
||||
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";
|
||||
import { SPLASH_ONLY } from "../utils/testUtils";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
describe("Moves - Burning Jealousy", () => {
|
||||
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
|
||||
.battleType("single")
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.ICE_SCALES)
|
||||
.enemyMoveset(Array(4).fill(Moves.HOWL))
|
||||
.startingLevel(10)
|
||||
.enemyLevel(10)
|
||||
.starterSpecies(Species.FEEBAS)
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.moveset([Moves.BURNING_JEALOUSY, Moves.GROWL]);
|
||||
|
||||
});
|
||||
|
||||
it("should burn the opponent if their stats were raised", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.BURNING_JEALOUSY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy.status?.effect).toBe(StatusEffect.BURN);
|
||||
}, TIMEOUT);
|
||||
|
||||
it("should still burn the opponent if their stats were both raised and lowered in the same turn", async () => {
|
||||
game.override
|
||||
.starterSpecies(0)
|
||||
.battleType("double");
|
||||
await game.classicMode.startBattle([Species.FEEBAS, Species.ABRA]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.BURNING_JEALOUSY);
|
||||
game.move.select(Moves.GROWL, 1);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy.status?.effect).toBe(StatusEffect.BURN);
|
||||
}, TIMEOUT);
|
||||
|
||||
it("should ignore stats raised by imposter", async () => {
|
||||
game.override
|
||||
.enemySpecies(Species.DITTO)
|
||||
.enemyAbility(Abilities.IMPOSTER)
|
||||
.enemyMoveset(SPLASH_ONLY);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.BURNING_JEALOUSY);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy.status?.effect).toBeUndefined();
|
||||
}, TIMEOUT);
|
||||
|
||||
it.skip("should ignore weakness policy", async () => { // TODO: Make this test if WP is implemented
|
||||
await game.classicMode.startBattle();
|
||||
}, TIMEOUT);
|
||||
|
||||
it("should be boosted by Sheer Force even if opponent didn't raise stats", async () => {
|
||||
game.override
|
||||
.ability(Abilities.SHEER_FORCE)
|
||||
.enemyMoveset(SPLASH_ONLY);
|
||||
vi.spyOn(allMoves[Moves.BURNING_JEALOUSY], "calculateBattlePower");
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
game.move.select(Moves.BURNING_JEALOUSY);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(allMoves[Moves.BURNING_JEALOUSY].calculateBattlePower).toHaveReturnedWith(allMoves[Moves.BURNING_JEALOUSY].power * 5461 / 4096);
|
||||
}, TIMEOUT);
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
import { BattlerIndex } from "#app/battle";
|
||||
import { allMoves } from "#app/data/move";
|
||||
import { Abilities } from "#app/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";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
describe("Moves - Lash Out", () => {
|
||||
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
|
||||
.battleType("single")
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.FUR_COAT)
|
||||
.enemyMoveset(Array(4).fill(Moves.GROWL))
|
||||
.startingLevel(10)
|
||||
.enemyLevel(10)
|
||||
.starterSpecies(Species.FEEBAS)
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.moveset([Moves.LASH_OUT]);
|
||||
|
||||
});
|
||||
|
||||
it("should deal double damage if the user's stats were lowered this turn", async () => {
|
||||
vi.spyOn(allMoves[Moves.LASH_OUT], "calculateBattlePower");
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
game.move.select(Moves.LASH_OUT);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(allMoves[Moves.LASH_OUT].calculateBattlePower).toHaveReturnedWith(150);
|
||||
}, TIMEOUT);
|
||||
});
|
Loading…
Reference in New Issue