mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-04-02 16:08:48 +01:00
[Bug] [Move] Add focus punch lost focus message (#5341)
* Add focus punch lost focus message * Rename attribute * Added automated test * Fix failedToTerrain being undefined * Update src/test/moves/focus_punch.test.ts Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com> * Update src/data/move.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --------- Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
parent
180a9cc054
commit
7a015e094f
@ -6633,7 +6633,7 @@ export function initAbilities() {
|
||||
.bypassFaint(),
|
||||
new Ability(Abilities.CORROSION, 7)
|
||||
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ Type.STEEL, Type.POISON ])
|
||||
.edgeCase(), // Should interact correctly with magic coat/bounce (not yet implemented) + fling with toxic orb (not implemented yet)
|
||||
.edgeCase(), // Should poison itself with toxic orb.
|
||||
new Ability(Abilities.COMATOSE, 7)
|
||||
.attr(UncopiableAbilityAbAttr)
|
||||
.attr(UnswappableAbilityAbAttr)
|
||||
|
@ -692,19 +692,17 @@ export default class Move implements Localizable {
|
||||
/**
|
||||
* Sees if a move has a custom failure text (by looking at each {@linkcode MoveAttr} of this move)
|
||||
* @param user {@linkcode Pokemon} using the move
|
||||
* @param target {@linkcode Pokemon} receiving the move
|
||||
* @param move {@linkcode Move} using the move
|
||||
* @param cancelled {@linkcode Utils.BooleanHolder} to hold boolean value
|
||||
* @param target {@linkcode Pokemon} target of the move
|
||||
* @param move {@linkcode Move} with this attribute
|
||||
* @returns string of the custom failure text, or `null` if it uses the default text ("But it failed!")
|
||||
*/
|
||||
getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null {
|
||||
getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined {
|
||||
for (const attr of this.attrs) {
|
||||
const failedText = attr.getFailedText(user, target, move, cancelled);
|
||||
if (failedText !== null) {
|
||||
const failedText = attr.getFailedText(user, target, move);
|
||||
if (failedText) {
|
||||
return failedText;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1089,11 +1087,10 @@ export abstract class MoveAttr {
|
||||
* @param user {@linkcode Pokemon} using the move
|
||||
* @param target {@linkcode Pokemon} target of the move
|
||||
* @param move {@linkcode Move} with this attribute
|
||||
* @param cancelled {@linkcode Utils.BooleanHolder} which stores if the move should fail
|
||||
* @returns the string representing failure of this {@linkcode Move}
|
||||
*/
|
||||
getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null {
|
||||
return null;
|
||||
getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1335,6 +1332,54 @@ export class PreMoveMessageAttr extends MoveAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute for moves that can be conditionally interrupted to be considered to
|
||||
* have failed before their "useMove" message is displayed. Currently used by
|
||||
* Focus Punch.
|
||||
* @extends MoveAttr
|
||||
*/
|
||||
export class PreUseInterruptAttr extends MoveAttr {
|
||||
protected message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string);
|
||||
protected overridesFailedMessage: boolean;
|
||||
protected conditionFunc: MoveConditionFunc;
|
||||
|
||||
/**
|
||||
* Create a new MoveInterruptedMessageAttr.
|
||||
* @param message The message to display when the move is interrupted, or a function that formats the message based on the user, target, and move.
|
||||
*/
|
||||
constructor(message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string), conditionFunc?: MoveConditionFunc) {
|
||||
super();
|
||||
this.message = message;
|
||||
this.conditionFunc = conditionFunc ?? (() => true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Message to display when a move is interrupted.
|
||||
* @param user {@linkcode Pokemon} using the move
|
||||
* @param target {@linkcode Pokemon} target of the move
|
||||
* @param move {@linkcode Move} with this attribute
|
||||
*/
|
||||
override apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
||||
return this.conditionFunc(user, target, move);
|
||||
}
|
||||
|
||||
/**
|
||||
* Message to display when a move is interrupted.
|
||||
* @param user {@linkcode Pokemon} using the move
|
||||
* @param target {@linkcode Pokemon} target of the move
|
||||
* @param move {@linkcode Move} with this attribute
|
||||
*/
|
||||
override getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined {
|
||||
if (this.message && this.conditionFunc(user, target, move)) {
|
||||
const message =
|
||||
typeof this.message === "string"
|
||||
? (this.message as string)
|
||||
: this.message(user, target, move);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute for Status moves that take attack type effectiveness
|
||||
* into consideration (i.e. {@linkcode https://bulbapedia.bulbagarden.net/wiki/Thunder_Wave_(move) | Thunder Wave})
|
||||
@ -1754,13 +1799,16 @@ export class AddSubstituteAttr extends MoveEffectAttr {
|
||||
return (user, target, move) => !user.getTag(SubstituteTag) && user.hp > Math.floor(user.getMaxHp() * this.hpCost) && user.getMaxHp() > 1;
|
||||
}
|
||||
|
||||
getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null {
|
||||
/**
|
||||
* Get the substitute-specific failure message if one should be displayed.
|
||||
* @param user The pokemon using the move.
|
||||
* @returns The substitute-specific failure message if the conditions apply, otherwise `undefined`
|
||||
*/
|
||||
getFailedText(user: Pokemon, _target: Pokemon, _move: Move): string | undefined {
|
||||
if (user.getTag(SubstituteTag)) {
|
||||
return i18next.t("moveTriggers:substituteOnOverlap", { pokemonName: getPokemonNameWithAffix(user) });
|
||||
} else if (user.hp <= Math.floor(user.getMaxHp() / 4) || user.getMaxHp() === 1) {
|
||||
return i18next.t("moveTriggers:substituteNotEnoughHp");
|
||||
} else {
|
||||
return i18next.t("battle:attackFailed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6230,10 +6278,12 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
return (user, target, move) => (move.category !== MoveCategory.STATUS || this.getSwitchOutCondition()(user, target, move));
|
||||
}
|
||||
|
||||
getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null {
|
||||
getFailedText(_user: Pokemon, target: Pokemon, _move: Move): string | undefined {
|
||||
const blockedByAbility = new Utils.BooleanHolder(false);
|
||||
applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility);
|
||||
return blockedByAbility.value ? i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) }) : null;
|
||||
if (blockedByAbility.value) {
|
||||
return i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) });
|
||||
}
|
||||
}
|
||||
|
||||
getSwitchOutCondition(): MoveConditionFunc {
|
||||
@ -9185,8 +9235,8 @@ export function initMoves() {
|
||||
.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()
|
||||
.condition((user, target, move) => !user.turnData.attacksReceived.find(r => r.damage)),
|
||||
.attr(PreUseInterruptAttr, i18next.t("moveTriggers:lostFocus"), user => !!user.turnData.attacksReceived.find(r => r.damage))
|
||||
.punchingMove(),
|
||||
new AttackMove(Moves.SMELLING_SALTS, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 3)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1)
|
||||
.attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS),
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
PokemonTypeChangeAbAttr,
|
||||
PostMoveUsedAbAttr,
|
||||
RedirectMoveAbAttr,
|
||||
ReduceStatusEffectDurationAbAttr
|
||||
ReduceStatusEffectDurationAbAttr,
|
||||
} from "#app/data/ability";
|
||||
import type { DelayedAttackTag } from "#app/data/arena-tag";
|
||||
import { CommonAnim } from "#app/data/battle-anims";
|
||||
@ -24,7 +24,8 @@ import {
|
||||
frenzyMissFunc,
|
||||
HealStatusEffectAttr,
|
||||
MoveFlags,
|
||||
PreMoveMessageAttr
|
||||
PreMoveMessageAttr,
|
||||
PreUseInterruptAttr,
|
||||
} from "#app/data/move";
|
||||
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms";
|
||||
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
|
||||
@ -42,7 +43,7 @@ import { MoveChargePhase } from "#app/phases/move-charge-phase";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
||||
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
|
||||
import { BooleanHolder, NumberHolder } from "#app/utils";
|
||||
import { NumberHolder } from "#app/utils";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
@ -293,7 +294,18 @@ export class MovePhase extends BattlePhase {
|
||||
}
|
||||
}
|
||||
|
||||
this.showMoveText();
|
||||
let success: boolean = true;
|
||||
// Check if there are any attributes that can interrupt the move, overriding the fail message.
|
||||
for (const move of this.move.getMove().getAttrs(PreUseInterruptAttr)) {
|
||||
if (move.apply(this.pokemon, targets[0], this.move.getMove())) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
this.showMoveText();
|
||||
}
|
||||
|
||||
if (moveQueue.length > 0) {
|
||||
// Using .shift here clears out two turn moves once they've been used
|
||||
@ -329,11 +341,14 @@ export class MovePhase extends BattlePhase {
|
||||
* Move conditions assume the move has a single target
|
||||
* TODO: is this sustainable?
|
||||
*/
|
||||
const passesConditions = move.applyConditions(this.pokemon, targets[0], move);
|
||||
const failedDueToWeather: boolean = globalScene.arena.isMoveWeatherCancelled(this.pokemon, move);
|
||||
const failedDueToTerrain: boolean = globalScene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, move);
|
||||
let failedDueToTerrain: boolean = false;
|
||||
if (success) {
|
||||
const passesConditions = move.applyConditions(this.pokemon, targets[0], move);
|
||||
const failedDueToWeather: boolean = globalScene.arena.isMoveWeatherCancelled(this.pokemon, move);
|
||||
failedDueToTerrain = globalScene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, move);
|
||||
success = passesConditions && !failedDueToWeather && !failedDueToTerrain;
|
||||
}
|
||||
|
||||
const success = passesConditions && !failedDueToWeather && !failedDueToTerrain;
|
||||
|
||||
// Update the battle's "last move" pointer, unless we're currently mimicking a move.
|
||||
if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) {
|
||||
@ -360,9 +375,8 @@ export class MovePhase extends BattlePhase {
|
||||
|
||||
this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual });
|
||||
|
||||
const failureMessage = move.getFailedText(this.pokemon, targets[0], move);
|
||||
let failedText: string | undefined;
|
||||
const failureMessage = move.getFailedText(this.pokemon, targets[0], move, new BooleanHolder(false));
|
||||
|
||||
if (failureMessage) {
|
||||
failedText = failureMessage;
|
||||
} else if (failedDueToTerrain) {
|
||||
@ -398,7 +412,7 @@ export class MovePhase extends BattlePhase {
|
||||
} else {
|
||||
this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual });
|
||||
|
||||
const failureMessage = move.getFailedText(this.pokemon, targets[0], move, new BooleanHolder(false));
|
||||
const failureMessage = move.getFailedText(this.pokemon, targets[0], move);
|
||||
this.showMoveText();
|
||||
this.showFailedText(failureMessage ?? undefined);
|
||||
|
||||
@ -566,7 +580,7 @@ export class MovePhase extends BattlePhase {
|
||||
applyMoveAttrs(PreMoveMessageAttr, this.pokemon, this.pokemon.getOpponents()[0], this.move.getMove());
|
||||
}
|
||||
|
||||
public showFailedText(failedText?: string): void {
|
||||
globalScene.queueMessage(failedText ?? i18next.t("battle:attackFailed"));
|
||||
public showFailedText(failedText: string = i18next.t("battle:attackFailed")): void {
|
||||
globalScene.queueMessage(failedText);
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,9 @@ import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import i18next from "i18next";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
|
||||
describe("Moves - Focus Punch", () => {
|
||||
@ -41,7 +42,7 @@ describe("Moves - Focus Punch", () => {
|
||||
it(
|
||||
"should deal damage at the end of turn if uninterrupted",
|
||||
async () => {
|
||||
await game.startBattle([ Species.CHARIZARD ]);
|
||||
await game.classicMode.startBattle([ Species.CHARIZARD ]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
@ -68,7 +69,7 @@ describe("Moves - Focus Punch", () => {
|
||||
async () => {
|
||||
game.override.enemyMoveset([ Moves.TACKLE ]);
|
||||
|
||||
await game.startBattle([ Species.CHARIZARD ]);
|
||||
await game.classicMode.startBattle([ Species.CHARIZARD ]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
@ -95,7 +96,7 @@ describe("Moves - Focus Punch", () => {
|
||||
async () => {
|
||||
game.override.enemyMoveset([ Moves.SPORE ]);
|
||||
|
||||
await game.startBattle([ Species.CHARIZARD ]);
|
||||
await game.classicMode.startBattle([ Species.CHARIZARD ]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
@ -119,7 +120,7 @@ describe("Moves - Focus Punch", () => {
|
||||
/** Guarantee a Trainer battle with multiple enemy Pokemon */
|
||||
game.override.startingWave(25);
|
||||
|
||||
await game.startBattle([ Species.CHARIZARD ]);
|
||||
await game.classicMode.startBattle([ Species.CHARIZARD ]);
|
||||
|
||||
game.forceEnemyToSwitch();
|
||||
game.move.select(Moves.FOCUS_PUNCH);
|
||||
@ -130,4 +131,15 @@ describe("Moves - Focus Punch", () => {
|
||||
expect(game.scene.phaseQueue.find(phase => phase instanceof MoveHeaderPhase)).toBeDefined();
|
||||
}
|
||||
);
|
||||
it("should replace the 'but it failed' text when the user gets hit", async () => {
|
||||
game.override.enemyMoveset([ Moves.TACKLE ]);
|
||||
await game.classicMode.startBattle([ Species.CHARIZARD ]);
|
||||
|
||||
game.move.select(Moves.FOCUS_PUNCH);
|
||||
await game.phaseInterceptor.to("MoveEndPhase", true);
|
||||
await game.phaseInterceptor.to("MessagePhase", false);
|
||||
const consoleSpy = vi.spyOn(console, "log");
|
||||
await game.phaseInterceptor.to("MoveEndPhase", true);
|
||||
expect(consoleSpy).nthCalledWith(1, i18next.t("moveTriggers:lostFocus"));
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user