[Move] Reimplement Beak Blast (#3427)
* Re-Implement Beak Blast * Fix charge animation loading issues
This commit is contained in:
parent
b5a40bfdfc
commit
b4a891cc71
|
@ -1,6 +1,6 @@
|
|||
//import { battleAnimRawData } from "./battle-anim-raw-data";
|
||||
import BattleScene from "../battle-scene";
|
||||
import { AttackMove, ChargeAttr, DelayedAttackAttr, MoveFlags, SelfStatusMove, allMoves } from "./move";
|
||||
import { AttackMove, BeakBlastHeaderAttr, ChargeAttr, DelayedAttackAttr, MoveFlags, SelfStatusMove, allMoves } from "./move";
|
||||
import Pokemon from "../field/pokemon";
|
||||
import * as Utils from "../utils";
|
||||
import { BattlerIndex } from "../battle";
|
||||
|
@ -499,7 +499,9 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
|
|||
} else {
|
||||
populateMoveAnim(move, ba);
|
||||
}
|
||||
const chargeAttr = allMoves[move].getAttrs(ChargeAttr)[0] || allMoves[move].getAttrs(DelayedAttackAttr)[0];
|
||||
const chargeAttr = allMoves[move].getAttrs(ChargeAttr)[0]
|
||||
|| allMoves[move].getAttrs(DelayedAttackAttr)[0]
|
||||
|| allMoves[move].getAttrs(BeakBlastHeaderAttr)[0];
|
||||
if (chargeAttr) {
|
||||
initMoveChargeAnim(scene, chargeAttr.chargeAnim).then(() => resolve());
|
||||
} else {
|
||||
|
@ -570,7 +572,9 @@ export function loadMoveAnimAssets(scene: BattleScene, moveIds: Moves[], startLo
|
|||
return new Promise(resolve => {
|
||||
const moveAnimations = moveIds.map(m => moveAnims.get(m) as AnimConfig).flat();
|
||||
for (const moveId of moveIds) {
|
||||
const chargeAttr = allMoves[moveId].getAttrs(ChargeAttr)[0] || allMoves[moveId].getAttrs(DelayedAttackAttr)[0];
|
||||
const chargeAttr = allMoves[moveId].getAttrs(ChargeAttr)[0]
|
||||
|| allMoves[moveId].getAttrs(DelayedAttackAttr)[0]
|
||||
|| allMoves[moveId].getAttrs(BeakBlastHeaderAttr)[0];
|
||||
if (chargeAttr) {
|
||||
const moveChargeAnims = chargeAnims.get(chargeAttr.chargeAnim);
|
||||
moveAnimations.push(moveChargeAnims instanceof AnimConfig ? moveChargeAnims : moveChargeAnims![0]); // TODO: is the bang correct?
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CommonAnim, CommonBattleAnim } from "./battle-anims";
|
||||
import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "./battle-anims";
|
||||
import { CommonAnimPhase, MoveEffectPhase, MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangeCallback, StatChangePhase } from "../phases";
|
||||
import { getPokemonNameWithAffix } from "../messages";
|
||||
import Pokemon, { MoveResult, HitResult } from "../field/pokemon";
|
||||
|
@ -118,6 +118,44 @@ export class RechargingTag extends BattlerTag {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BattlerTag representing the "charge phase" of Beak Blast
|
||||
* Pokemon with this tag will inflict BURN status on any attacker that makes contact.
|
||||
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Beak_Blast_(move) | Beak Blast}
|
||||
*/
|
||||
export class BeakBlastChargingTag extends BattlerTag {
|
||||
constructor() {
|
||||
super(BattlerTagType.BEAK_BLAST_CHARGING, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END ], 1, Moves.BEAK_BLAST);
|
||||
}
|
||||
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
// Play Beak Blast's charging animation
|
||||
new MoveChargeAnim(ChargeAnim.BEAK_BLAST_CHARGING, this.sourceMove, pokemon).play(pokemon.scene);
|
||||
|
||||
// Queue Beak Blast's header message
|
||||
pokemon.scene.queueMessage(i18next.t("moveTriggers:startedHeatingUpBeak", { pokemonName: getPokemonNameWithAffix(pokemon) }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflicts `BURN` status on attackers that make contact, and causes this tag
|
||||
* to be removed after the source makes a move (or the turn ends, whichever comes first)
|
||||
* @param pokemon {@linkcode Pokemon} the owner of this tag
|
||||
* @param lapseType {@linkcode BattlerTagLapseType} the type of functionality invoked in battle
|
||||
* @returns `true` if invoked with the CUSTOM lapse type; `false` otherwise
|
||||
*/
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (lapseType === BattlerTagLapseType.CUSTOM) {
|
||||
const effectPhase = pokemon.scene.getCurrentPhase();
|
||||
if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) {
|
||||
const attacker = effectPhase.getPokemon();
|
||||
attacker.trySetStatus(StatusEffect.BURN, true, pokemon);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.lapse(pokemon, lapseType);
|
||||
}
|
||||
}
|
||||
|
||||
export class TrappedTag extends BattlerTag {
|
||||
constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType, turnCount: number, sourceMove: Moves, sourceId: number) {
|
||||
super(tagType, lapseType, turnCount, sourceMove, sourceId);
|
||||
|
@ -1738,6 +1776,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
|||
switch (tagType) {
|
||||
case BattlerTagType.RECHARGING:
|
||||
return new RechargingTag(sourceMove);
|
||||
case BattlerTagType.BEAK_BLAST_CHARGING:
|
||||
return new BeakBlastChargingTag();
|
||||
case BattlerTagType.FLINCHED:
|
||||
return new FlinchedTag(sourceMove);
|
||||
case BattlerTagType.INTERRUPTED:
|
||||
|
|
|
@ -1021,6 +1021,22 @@ export class MessageHeaderAttr extends MoveHeaderAttr {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Header attribute to implement the "charge phase" of Beak Blast at the
|
||||
* beginning of a turn.
|
||||
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Beak_Blast_(move) | Beak Blast}
|
||||
* @see {@linkcode BeakBlastChargingTag}
|
||||
*/
|
||||
export class BeakBlastHeaderAttr extends MoveHeaderAttr {
|
||||
/** Required to initialize Beak Blast's charge animation correctly */
|
||||
public chargeAnim = ChargeAnim.BEAK_BLAST_CHARGING;
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
user.addTag(BattlerTagType.BEAK_BLAST_CHARGING);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class PreMoveMessageAttr extends MoveAttr {
|
||||
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string);
|
||||
|
||||
|
@ -2391,24 +2407,21 @@ export class ChargeAttr extends OverrideMoveEffectAttr {
|
|||
private chargeText: string;
|
||||
private tagType: BattlerTagType | null;
|
||||
private chargeEffect: boolean;
|
||||
public sameTurn: boolean;
|
||||
public followUpPriority: integer | null;
|
||||
|
||||
constructor(chargeAnim: ChargeAnim, chargeText: string, tagType?: BattlerTagType | null, chargeEffect: boolean = false, sameTurn: boolean = false, followUpPriority?: integer) {
|
||||
constructor(chargeAnim: ChargeAnim, chargeText: string, tagType?: BattlerTagType | null, chargeEffect: boolean = false) {
|
||||
super();
|
||||
|
||||
this.chargeAnim = chargeAnim;
|
||||
this.chargeText = chargeText;
|
||||
this.tagType = tagType!; // TODO: is this bang correct?
|
||||
this.chargeEffect = chargeEffect;
|
||||
this.sameTurn = sameTurn;
|
||||
this.followUpPriority = followUpPriority!; // TODO: is this bang correct?
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const lastMove = user.getLastXMoves().find(() => true);
|
||||
if (!lastMove || lastMove.move !== move.id || (lastMove.result !== MoveResult.OTHER && (this.sameTurn || lastMove.turn !== user.scene.currentBattle.turn))) {
|
||||
if (!lastMove || lastMove.move !== move.id || (lastMove.result !== MoveResult.OTHER && lastMove.turn !== user.scene.currentBattle.turn)) {
|
||||
(args[0] as Utils.BooleanHolder).value = true;
|
||||
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, () => {
|
||||
user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
|
||||
|
@ -2420,13 +2433,6 @@ export class ChargeAttr extends OverrideMoveEffectAttr {
|
|||
}
|
||||
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
|
||||
user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true });
|
||||
if (this.sameTurn) {
|
||||
let movesetMove = user.moveset.find(m => m?.moveId === move.id);
|
||||
if (!movesetMove) { // account for any move that calls a ChargeAttr move when the ChargeAttr move does not exist in moveset
|
||||
movesetMove = new PokemonMove(move.id, 0, 0, true);
|
||||
}
|
||||
user.scene.pushMovePhase(new MovePhase(user.scene, user, [ target.getBattlerIndex() ], movesetMove, true), this.followUpPriority!); // TODO: is this bang correct?
|
||||
}
|
||||
user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id);
|
||||
resolve(true);
|
||||
});
|
||||
|
@ -8081,11 +8087,10 @@ export function initMoves() {
|
|||
.attr(StatChangeAttr, BattleStat.ATK, -1),
|
||||
new StatusMove(Moves.INSTRUCT, Type.PSYCHIC, -1, 15, -1, 0, 7)
|
||||
.unimplemented(),
|
||||
new AttackMove(Moves.BEAK_BLAST, Type.FLYING, MoveCategory.PHYSICAL, 100, 100, 15, -1, 5, 7)
|
||||
.attr(ChargeAttr, ChargeAnim.BEAK_BLAST_CHARGING, i18next.t("moveTriggers:startedHeatingUpBeak", {pokemonName: "{USER}"}), undefined, false, true, -3)
|
||||
new AttackMove(Moves.BEAK_BLAST, Type.FLYING, MoveCategory.PHYSICAL, 100, 100, 15, -1, -3, 7)
|
||||
.attr(BeakBlastHeaderAttr)
|
||||
.ballBombMove()
|
||||
.makesContact(false)
|
||||
.partial(),
|
||||
.makesContact(false),
|
||||
new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, -1, 0, 7)
|
||||
.attr(StatChangeAttr, BattleStat.DEF, -1, true, null, true, false, MoveEffectTrigger.HIT, true)
|
||||
.soundBased()
|
||||
|
|
|
@ -66,5 +66,6 @@ export enum BattlerTagType {
|
|||
IGNORE_GHOST = "IGNORE_GHOST",
|
||||
IGNORE_DARK = "IGNORE_DARK",
|
||||
GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA",
|
||||
GULP_MISSILE_PIKACHU = "GULP_MISSILE_PIKACHU"
|
||||
GULP_MISSILE_PIKACHU = "GULP_MISSILE_PIKACHU",
|
||||
BEAK_BLAST_CHARGING = "BEAK_BLAST_CHARGING"
|
||||
}
|
||||
|
|
|
@ -3087,6 +3087,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||
Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT
|
||||
&& (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => {
|
||||
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
|
||||
target.lapseTag(BattlerTagType.BEAK_BLAST_CHARGING);
|
||||
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
|
||||
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { Species } from "#enums/species";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "#test/utils/gameManagerUtils";
|
||||
import { BerryPhase, MovePhase, TurnEndPhase } from "#app/phases";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
|
||||
import { StatusEffect } from "#app/enums/status-effect.js";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
describe("Moves - Beak Blast", () => {
|
||||
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")
|
||||
.ability(Abilities.UNNERVE)
|
||||
.moveset([Moves.BEAK_BLAST])
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyAbility(Abilities.INSOMNIA)
|
||||
.enemyMoveset(Array(4).fill(Moves.TACKLE))
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
it(
|
||||
"should add a charge effect that burns attackers on contact",
|
||||
async () => {
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.BEAK_BLAST));
|
||||
|
||||
await game.phaseInterceptor.to(MovePhase, false);
|
||||
expect(leadPokemon.getTag(BattlerTagType.BEAK_BLAST_CHARGING)).toBeDefined();
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
expect(enemyPokemon.status?.effect).toBe(StatusEffect.BURN);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should still charge and burn opponents if the user is sleeping",
|
||||
async () => {
|
||||
game.override.statusEffect(StatusEffect.SLEEP);
|
||||
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.BEAK_BLAST));
|
||||
|
||||
await game.phaseInterceptor.to(MovePhase, false);
|
||||
expect(leadPokemon.getTag(BattlerTagType.BEAK_BLAST_CHARGING)).toBeDefined();
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
expect(enemyPokemon.status?.effect).toBe(StatusEffect.BURN);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should not burn attackers that don't make contact",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.WATER_GUN));
|
||||
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.BEAK_BLAST));
|
||||
|
||||
await game.phaseInterceptor.to(MovePhase, false);
|
||||
expect(leadPokemon.getTag(BattlerTagType.BEAK_BLAST_CHARGING)).toBeDefined();
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
expect(enemyPokemon.status?.effect).not.toBe(StatusEffect.BURN);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should only hit twice with Multi-Lens",
|
||||
async () => {
|
||||
game.override.startingHeldItems([{name: "MULTI_LENS", count: 1}]);
|
||||
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.BEAK_BLAST));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should be blocked by Protect",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.PROTECT));
|
||||
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.BEAK_BLAST));
|
||||
|
||||
await game.phaseInterceptor.to(MovePhase, false);
|
||||
expect(leadPokemon.getTag(BattlerTagType.BEAK_BLAST_CHARGING)).toBeDefined();
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
expect(leadPokemon.getTag(BattlerTagType.BEAK_BLAST_CHARGING)).toBeUndefined();
|
||||
}, TIMEOUT
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue