[Enhancement] Add Move Header phase and attributes (#2716)
* Create Move Header phase and attributes * Fix move header persisting after run/ball * Add mid-turn sleep test * Fix status effect text in move header phase * Remove preemptive non-volatile status check * Process move headers in main loop of TurnStartPhase instead * Fix merge issues in Focus Punch test * Fix Focus Punch test + ESLint * Add i18n key for Focus Punch header message * Fix missing arg in i18n message * Add Focus Punch message translations (DE, FR, KO, PT-BR, ZH) Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> Co-authored-by: Enoch <enoch.jwsong@gmail.com> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: Sonny Ding <93831983+sonnyding1@users.noreply.github.com> * Update src/locales/it/move-trigger.ts Co-authored-by: Enoch <enoch.jwsong@gmail.com> * Use new test helper functions + snooz's cleanup suggestions * Add check for MoveHeaderPhase in switch test * Add key to JA locale * Add CA-ES locale key * Fix strict-null checks in focus punch test --------- Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> Co-authored-by: Enoch <enoch.jwsong@gmail.com> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: Sonny Ding <93831983+sonnyding1@users.noreply.github.com>
This commit is contained in:
parent
f9d7b71fd6
commit
2b99f005dc
|
@ -984,6 +984,43 @@ export class MoveEffectAttr extends MoveAttr {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class defining all Move Header attributes.
|
||||
* Move Header effects apply at the beginning of a turn before any moves are resolved.
|
||||
* They can be used to apply effects to the field (e.g. queueing a message) or to the user
|
||||
* (e.g. adding a battler tag).
|
||||
*/
|
||||
export class MoveHeaderAttr extends MoveAttr {
|
||||
constructor() {
|
||||
super(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Header attribute to queue a message at the beginning of a turn.
|
||||
* @see {@link MoveHeaderAttr}
|
||||
*/
|
||||
export class MessageHeaderAttr extends MoveHeaderAttr {
|
||||
private message: string | ((user: Pokemon, move: Move) => string);
|
||||
|
||||
constructor(message: string | ((user: Pokemon, move: Move) => string)) {
|
||||
super();
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const message = typeof this.message === "string"
|
||||
? this.message
|
||||
: this.message(user, move);
|
||||
|
||||
if (message) {
|
||||
user.scene.queueMessage(message);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class PreMoveMessageAttr extends MoveAttr {
|
||||
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string);
|
||||
|
||||
|
@ -6837,6 +6874,7 @@ export function initMoves() {
|
|||
&& (user.status.effect === StatusEffect.BURN || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.PARALYSIS) ? 2 : 1)
|
||||
.attr(BypassBurnDamageReductionAttr),
|
||||
new AttackMove(Moves.FOCUS_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 20, -1, -3, 3)
|
||||
.attr(MessageHeaderAttr, (user, move) => i18next.t("moveTriggers:isTighteningFocus", {pokemonName: getPokemonNameWithAffix(user)}))
|
||||
.punchingMove()
|
||||
.ignoresVirtual()
|
||||
.condition((user, target, move) => !user.turnData.attacksReceived.find(r => r.damage)),
|
||||
|
|
|
@ -21,6 +21,7 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"isGlowing": "{{pokemonName}} became cloaked in a harsh light!",
|
||||
"bellChimed": "A bell chimed!",
|
||||
"foresawAnAttack": "{{pokemonName}} foresaw\nan attack!",
|
||||
"isTighteningFocus": "{{pokemonName}} is\ntightening its focus!",
|
||||
"hidUnderwater": "{{pokemonName}} hid\nunderwater!",
|
||||
"soothingAromaWaftedThroughArea": "A soothing aroma wafted through the area!",
|
||||
"sprangUp": "{{pokemonName}} sprang up!",
|
||||
|
|
|
@ -21,6 +21,7 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"isGlowing": "{{pokemonName}} leuchtet grell!",
|
||||
"bellChimed": "Eine Glocke läutet!",
|
||||
"foresawAnAttack": "{{pokemonName}} sieht einen Angriff voraus!",
|
||||
"isTighteningFocus": "{{pokemonName}} konzentriert sich!",
|
||||
"hidUnderwater": "{{pokemonName}} taucht unter!",
|
||||
"soothingAromaWaftedThroughArea": "Ein wohltuendes Aroma breitet sich aus!",
|
||||
"sprangUp": "{{pokemonName}} springt hoch in die Luft!",
|
||||
|
|
|
@ -21,6 +21,7 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"isGlowing": "{{pokemonName}} became cloaked in a harsh light!",
|
||||
"bellChimed": "A bell chimed!",
|
||||
"foresawAnAttack": "{{pokemonName}} foresaw\nan attack!",
|
||||
"isTighteningFocus": "{{pokemonName}} is\ntightening its focus!",
|
||||
"hidUnderwater": "{{pokemonName}} hid\nunderwater!",
|
||||
"soothingAromaWaftedThroughArea": "A soothing aroma wafted through the area!",
|
||||
"sprangUp": "{{pokemonName}} sprang up!",
|
||||
|
|
|
@ -21,6 +21,7 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"isGlowing": "{{pokemonName}} became cloaked in a harsh light!",
|
||||
"bellChimed": "A bell chimed!",
|
||||
"foresawAnAttack": "{{pokemonName}} foresaw\nan attack!",
|
||||
"isTighteningFocus": "{{pokemonName}} is\ntightening its focus!",
|
||||
"hidUnderwater": "{{pokemonName}} hid\nunderwater!",
|
||||
"soothingAromaWaftedThroughArea": "A soothing aroma wafted through the area!",
|
||||
"sprangUp": "{{pokemonName}} sprang up!",
|
||||
|
|
|
@ -21,6 +21,7 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"isGlowing": "{{pokemonName}} est entouré\nd’une lumière intense !",
|
||||
"bellChimed": "Un grelot sonne !",
|
||||
"foresawAnAttack": "{{pokemonName}}\nprévoit une attaque !",
|
||||
"isTighteningFocus": "{{pokemonName}} se concentre\nau maximum !",
|
||||
"hidUnderwater": "{{pokemonName}}\nse cache sous l’eau !",
|
||||
"soothingAromaWaftedThroughArea": "Une odeur apaisante flotte dans l’air !",
|
||||
"sprangUp": "{{pokemonName}}\nse propulse dans les airs !",
|
||||
|
|
|
@ -21,6 +21,7 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"isGlowing": "{{pokemonName}} è avvolto da una luce intensa!",
|
||||
"bellChimed": " Si sente suonare una campanella!",
|
||||
"foresawAnAttack": "{{pokemonName}} presagisce\nl’attacco imminente!",
|
||||
"isTighteningFocus": "{{pokemonName}} si concentra al massimo!",
|
||||
"hidUnderwater": "{{pokemonName}} sparisce\nsott’acqua!",
|
||||
"soothingAromaWaftedThroughArea": "Un gradevole profumo si diffonde nell’aria!",
|
||||
"sprangUp": "{{pokemonName}} spicca un gran balzo!",
|
||||
|
|
|
@ -21,6 +21,7 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"isGlowing": "{{pokemonName}}を\nはげしいひかりが つつむ!",
|
||||
"bellChimed": "すずのおとが ひびきわたった!",
|
||||
"foresawAnAttack": "{{pokemonName}}は\nみらいに こうげきを よちした!",
|
||||
"isTighteningFocus": "{{pokemonName}} is\ntightening its focus!",
|
||||
"hidUnderwater": "{{pokemonName}}は\nすいちゅうに みをひそめた!",
|
||||
"soothingAromaWaftedThroughArea": "ここちよい かおりが ひろがった!",
|
||||
"sprangUp": "{{pokemonName}}は\nたかく とびはねた!",
|
||||
|
|
|
@ -21,6 +21,7 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"isGlowing": "{{pokemonName}}를(을)\n강렬한 빛이 감쌌다!",
|
||||
"bellChimed": "방울소리가 울려 퍼졌다!",
|
||||
"foresawAnAttack": "{{pokemonName}}는(은)\n미래의 공격을 예지했다!",
|
||||
"isTighteningFocus": "{{pokemonName}}[[는]]\n집중력을 높이고 있다!",
|
||||
"hidUnderwater": "{{pokemonName}}는(은)\n물속에 몸을 숨겼다!",
|
||||
"soothingAromaWaftedThroughArea": "기분 좋은 향기가 퍼졌다!",
|
||||
"sprangUp": "{{pokemonName}}는(은)\n높이 뛰어올랐다!",
|
||||
|
|
|
@ -21,6 +21,7 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"isGlowing": "{{pokemonName}} ficou envolto em uma luz forte!",
|
||||
"bellChimed": "Um sino tocou!",
|
||||
"foresawAnAttack": "{{pokemonName}} previu/num ataque!",
|
||||
"isTighteningFocus": "{{pokemonName}} está\naumentando seu foco!",
|
||||
"hidUnderwater": "{{pokemonName}} se escondeu/nembaixo d'água!",
|
||||
"soothingAromaWaftedThroughArea": "Um aroma suave se espalhou pelo ambiente!",
|
||||
"sprangUp": "{{pokemonName}} se levantou!",
|
||||
|
|
|
@ -21,6 +21,7 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"isGlowing": "强光包围了{{pokemonName}}\n!",
|
||||
"bellChimed": "铃声响彻四周!",
|
||||
"foresawAnAttack": "{{pokemonName}}\n预知了未来的攻击!",
|
||||
"isTighteningFocus": "{{pokemonName}}正在集中注意力!",
|
||||
"hidUnderwater": "{{pokemonName}}\n潜入了水中!",
|
||||
"soothingAromaWaftedThroughArea": "怡人的香气扩散了开来!",
|
||||
"sprangUp": "{{pokemonName}}\n高高地跳了起来!",
|
||||
|
|
|
@ -21,6 +21,7 @@ export const moveTriggers: SimpleTranslationEntries = {
|
|||
"isGlowing": "強光包圍了\n{{pokemonName}}!",
|
||||
"bellChimed": "鈴聲響徹四周!",
|
||||
"foresawAnAttack": "{{pokemonName}}\n預知了未來的攻擊!",
|
||||
"isTighteningFocus": "{{pokemonName}}正在集中注意力!",
|
||||
"hidUnderwater": "{{pokemonName}}\n潛入了水中!",
|
||||
"soothingAromaWaftedThroughArea": "怡人的香氣擴散了開來!",
|
||||
"sprangUp": "{{pokemonName}}\n高高地跳了起來!",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import BattleScene, { bypassLogin } from "./battle-scene";
|
||||
import { default as Pokemon, PlayerPokemon, EnemyPokemon, PokemonMove, MoveResult, DamageResult, FieldPosition, HitResult, TurnMove } from "./field/pokemon";
|
||||
import * as Utils from "./utils";
|
||||
import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr } from "./data/move";
|
||||
import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr, MoveHeaderAttr } from "./data/move";
|
||||
import { Mode } from "./ui/ui";
|
||||
import { Command } from "./ui/command-ui-handler";
|
||||
import { Stat } from "./data/pokemon-stat";
|
||||
|
@ -2353,6 +2353,9 @@ export class TurnStartPhase extends FieldPhase {
|
|||
continue;
|
||||
}
|
||||
const move = pokemon.getMoveset().find(m => m?.moveId === queuedMove.move) || new PokemonMove(queuedMove.move);
|
||||
if (move.getMove().hasAttr(MoveHeaderAttr)) {
|
||||
this.scene.unshiftPhase(new MoveHeaderPhase(this.scene, pokemon, move));
|
||||
}
|
||||
if (pokemon.isPlayer()) {
|
||||
if (turnCommand.cursor === -1) {
|
||||
this.scene.pushPhase(new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move!.targets, move));//TODO: is the bang correct here?
|
||||
|
@ -2597,6 +2600,32 @@ export class CommonAnimPhase extends PokemonPhase {
|
|||
}
|
||||
}
|
||||
|
||||
export class MoveHeaderPhase extends BattlePhase {
|
||||
public pokemon: Pokemon;
|
||||
public move: PokemonMove;
|
||||
|
||||
constructor(scene: BattleScene, pokemon: Pokemon, move: PokemonMove) {
|
||||
super(scene);
|
||||
|
||||
this.pokemon = pokemon;
|
||||
this.move = move;
|
||||
}
|
||||
|
||||
canMove(): boolean {
|
||||
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon);
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
if (this.canMove()) {
|
||||
applyMoveAttrs(MoveHeaderAttr, this.pokemon, null, this.move.getMove()).then(() => this.end());
|
||||
} else {
|
||||
this.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MovePhase extends BattlePhase {
|
||||
public pokemon: Pokemon;
|
||||
public move: PokemonMove;
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
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, MessagePhase, MoveHeaderPhase, SwitchSummonPhase, TurnStartPhase } from "#app/phases";
|
||||
import { SPLASH_ONLY } from "#test/utils/testUtils";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
describe("Moves - Focus Punch", () => {
|
||||
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.FOCUS_PUNCH])
|
||||
.enemySpecies(Species.GROUDON)
|
||||
.enemyAbility(Abilities.INSOMNIA)
|
||||
.enemyMoveset(SPLASH_ONLY)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
it(
|
||||
"should deal damage at the end of turn if uninterrupted",
|
||||
async () => {
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
const enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.FOCUS_PUNCH));
|
||||
|
||||
await game.phaseInterceptor.to(MessagePhase);
|
||||
|
||||
expect(enemyPokemon.hp).toBe(enemyStartingHp);
|
||||
expect(leadPokemon.getMoveHistory().length).toBe(0);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
|
||||
expect(leadPokemon.getMoveHistory().length).toBe(1);
|
||||
expect(leadPokemon.turnData.damageDealt).toBe(enemyStartingHp - enemyPokemon.hp);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should fail if the user is hit",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
const enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.FOCUS_PUNCH));
|
||||
|
||||
await game.phaseInterceptor.to(MessagePhase);
|
||||
|
||||
expect(enemyPokemon.hp).toBe(enemyStartingHp);
|
||||
expect(leadPokemon.getMoveHistory().length).toBe(0);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(enemyPokemon.hp).toBe(enemyStartingHp);
|
||||
expect(leadPokemon.getMoveHistory().length).toBe(1);
|
||||
expect(leadPokemon.turnData.damageDealt).toBe(0);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should be cancelled if the user falls asleep mid-turn",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.SPORE));
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.FOCUS_PUNCH));
|
||||
|
||||
await game.phaseInterceptor.to(MessagePhase); // Header message
|
||||
|
||||
expect(leadPokemon.getMoveHistory().length).toBe(0);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon.getMoveHistory().length).toBe(1);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should not queue its pre-move message before an enemy switches",
|
||||
async () => {
|
||||
/** Guarantee a Trainer battle with multiple enemy Pokemon */
|
||||
game.override.startingWave(25);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
game.forceOpponentToSwitch();
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.FOCUS_PUNCH));
|
||||
|
||||
await game.phaseInterceptor.to(TurnStartPhase);
|
||||
|
||||
expect(game.scene.getCurrentPhase() instanceof SwitchSummonPhase).toBeTruthy();
|
||||
expect(game.scene.phaseQueue.find(phase => phase instanceof MoveHeaderPhase)).toBeDefined();
|
||||
}, TIMEOUT
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue