[Refactor] Cleaning up Learn move phase (#3672)
* Learn Move Phase rewrite * Typedocs * messages with confirm do not need an extra button press no more * Added Documentation * This does not work * so sad * Some updates * Eslint issues + clean up * Additions to handle learning during evolution + test fixes * some more checks * Update src/overrides.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/test/phases/learn-move-phase.test.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Added new function and updated tests * Fixed bracketing and added parameter types * Added Sketch to the conditional * Added some fixes. Weird stuff going on. * Whoops * async implementation done * Update src/phases/learn-move-phase.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Made showText=> summary a promise * adapt learn-move-phase to `async-await` * await add --------- Co-authored-by: frutescens <info@laptop> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
This commit is contained in:
parent
f5bf766ff7
commit
26eb63cf67
|
@ -1,6 +1,6 @@
|
|||
import BattleScene from "#app/battle-scene";
|
||||
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
|
||||
import { allMoves } from "#app/data/move";
|
||||
import Move, { allMoves } from "#app/data/move";
|
||||
import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
|
@ -9,14 +9,15 @@ import { SummaryUiMode } from "#app/ui/summary-ui-handler";
|
|||
import { Mode } from "#app/ui/ui";
|
||||
import i18next from "i18next";
|
||||
import { PlayerPartyMemberPokemonPhase } from "./player-party-member-pokemon-phase";
|
||||
import Pokemon from "#app/field/pokemon";
|
||||
|
||||
export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
|
||||
private moveId: Moves;
|
||||
private messageMode: Mode;
|
||||
private fromTM: boolean;
|
||||
|
||||
constructor(scene: BattleScene, partyMemberIndex: integer, moveId: Moves, fromTM?: boolean) {
|
||||
super(scene, partyMemberIndex);
|
||||
|
||||
this.moveId = moveId;
|
||||
this.fromTM = fromTM ?? false;
|
||||
}
|
||||
|
@ -26,87 +27,128 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
|
|||
|
||||
const pokemon = this.getPokemon();
|
||||
const move = allMoves[this.moveId];
|
||||
const currentMoveset = pokemon.getMoveset();
|
||||
|
||||
const existingMoveIndex = pokemon.getMoveset().findIndex(m => m?.moveId === move.id);
|
||||
|
||||
if (existingMoveIndex > -1) {
|
||||
// The game first checks if the Pokemon already has the move and ends the phase if it does.
|
||||
const hasMoveAlready = currentMoveset.some(m => m?.moveId === move.id) && this.moveId !== Moves.SKETCH;
|
||||
if (hasMoveAlready) {
|
||||
return this.end();
|
||||
}
|
||||
|
||||
const emptyMoveIndex = pokemon.getMoveset().length < 4
|
||||
? pokemon.getMoveset().length
|
||||
: pokemon.getMoveset().findIndex(m => m === null);
|
||||
|
||||
const messageMode = this.scene.ui.getHandler() instanceof EvolutionSceneHandler
|
||||
? Mode.EVOLUTION_SCENE
|
||||
: Mode.MESSAGE;
|
||||
|
||||
if (emptyMoveIndex > -1) {
|
||||
pokemon.setMove(emptyMoveIndex, this.moveId);
|
||||
if (this.fromTM) {
|
||||
pokemon.usedTMs.push(this.moveId);
|
||||
}
|
||||
initMoveAnim(this.scene, this.moveId).then(() => {
|
||||
loadMoveAnimAssets(this.scene, [this.moveId], true)
|
||||
.then(() => {
|
||||
this.scene.ui.setMode(messageMode).then(() => {
|
||||
// Sound loaded into game as is
|
||||
this.scene.playSound("level_up_fanfare");
|
||||
this.scene.ui.showText(i18next.t("battle:learnMove", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name }), null, () => {
|
||||
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeMoveLearnedTrigger, true);
|
||||
this.end();
|
||||
}, messageMode === Mode.EVOLUTION_SCENE ? 1000 : null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
this.messageMode = this.scene.ui.getHandler() instanceof EvolutionSceneHandler ? Mode.EVOLUTION_SCENE : Mode.MESSAGE;
|
||||
this.scene.ui.setMode(this.messageMode);
|
||||
// If the Pokemon has less than 4 moves, the new move is added to the largest empty moveset index
|
||||
// If it has 4 moves, the phase then checks if the player wants to replace the move itself.
|
||||
if (currentMoveset.length < 4) {
|
||||
this.learnMove(currentMoveset.length, move, pokemon);
|
||||
} else {
|
||||
this.scene.ui.setMode(messageMode).then(() => {
|
||||
this.scene.ui.showText(i18next.t("battle:learnMovePrompt", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name }), null, () => {
|
||||
this.scene.ui.showText(i18next.t("battle:learnMoveLimitReached", { pokemonName: getPokemonNameWithAffix(pokemon) }), null, () => {
|
||||
this.scene.ui.showText(i18next.t("battle:learnMoveReplaceQuestion", { moveName: move.name }), null, () => {
|
||||
const noHandler = () => {
|
||||
this.scene.ui.setMode(messageMode).then(() => {
|
||||
this.scene.ui.showText(i18next.t("battle:learnMoveStopTeaching", { moveName: move.name }), null, () => {
|
||||
this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => {
|
||||
this.scene.ui.setMode(messageMode);
|
||||
this.scene.ui.showText(i18next.t("battle:learnMoveNotLearned", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name }), null, () => this.end(), null, true);
|
||||
}, () => {
|
||||
this.scene.ui.setMode(messageMode);
|
||||
this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, this.moveId));
|
||||
this.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => {
|
||||
this.scene.ui.setMode(messageMode);
|
||||
this.scene.ui.showText(i18next.t("battle:learnMoveForgetQuestion"), null, () => {
|
||||
this.scene.ui.setModeWithoutClear(Mode.SUMMARY, this.getPokemon(), SummaryUiMode.LEARN_MOVE, move, (moveIndex: integer) => {
|
||||
if (moveIndex === 4) {
|
||||
noHandler();
|
||||
return;
|
||||
}
|
||||
this.scene.ui.setMode(messageMode).then(() => {
|
||||
this.scene.ui.showText(i18next.t("battle:countdownPoof"), null, () => {
|
||||
this.scene.ui.showText(i18next.t("battle:learnMoveForgetSuccess", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: pokemon.moveset[moveIndex]!.getName() }), null, () => { // TODO: is the bang correct?
|
||||
this.scene.ui.showText(i18next.t("battle:learnMoveAnd"), null, () => {
|
||||
if (this.fromTM) {
|
||||
pokemon.usedTMs.push(this.moveId);
|
||||
}
|
||||
pokemon.setMove(moveIndex, Moves.NONE);
|
||||
this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, this.moveId));
|
||||
this.end();
|
||||
}, null, true);
|
||||
}, null, true);
|
||||
}, null, true);
|
||||
});
|
||||
});
|
||||
}, null, true);
|
||||
}, noHandler);
|
||||
});
|
||||
}, null, true);
|
||||
}, null, true);
|
||||
});
|
||||
this.replaceMoveCheck(move, pokemon);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This displays a chain of messages (listed below) and asks if the user wishes to forget a move.
|
||||
*
|
||||
* > [Pokemon] wants to learn the move [MoveName]
|
||||
* > However, [Pokemon] already knows four moves.
|
||||
* > Should a move be forgotten and replaced with [MoveName]? --> `Mode.CONFIRM` -> Yes: Go to `this.forgetMoveProcess()`, No: Go to `this.rejectMoveAndEnd()`
|
||||
* @param move The Move to be learned
|
||||
* @param Pokemon The Pokemon learning the move
|
||||
*/
|
||||
async replaceMoveCheck(move: Move, pokemon: Pokemon) {
|
||||
const learnMovePrompt = i18next.t("battle:learnMovePrompt", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name });
|
||||
const moveLimitReached = i18next.t("battle:learnMoveLimitReached", { pokemonName: getPokemonNameWithAffix(pokemon) });
|
||||
const shouldReplaceQ = i18next.t("battle:learnMoveReplaceQuestion", { moveName: move.name });
|
||||
const preQText = [learnMovePrompt, moveLimitReached].join("$");
|
||||
await this.scene.ui.showTextPromise(preQText);
|
||||
await this.scene.ui.showTextPromise(shouldReplaceQ, undefined, false);
|
||||
await this.scene.ui.setModeWithoutClear(Mode.CONFIRM,
|
||||
() => this.forgetMoveProcess(move, pokemon), // Yes
|
||||
() => { // No
|
||||
this.scene.ui.setMode(this.messageMode);
|
||||
this.rejectMoveAndEnd(move, pokemon);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This facilitates the process in which an old move is chosen to be forgotten.
|
||||
*
|
||||
* > Which move should be forgotten?
|
||||
*
|
||||
* The game then goes `Mode.SUMMARY` to select a move to be forgotten.
|
||||
* If a player does not select a move or chooses the new move (`moveIndex === 4`), the game goes to `this.rejectMoveAndEnd()`.
|
||||
* If an old move is selected, the function then passes the `moveIndex` to `this.learnMove()`
|
||||
* @param move The Move to be learned
|
||||
* @param Pokemon The Pokemon learning the move
|
||||
*/
|
||||
async forgetMoveProcess(move: Move, pokemon: Pokemon) {
|
||||
this.scene.ui.setMode(this.messageMode);
|
||||
await this.scene.ui.showTextPromise(i18next.t("battle:learnMoveForgetQuestion"), undefined, true);
|
||||
await this.scene.ui.setModeWithoutClear(Mode.SUMMARY, pokemon, SummaryUiMode.LEARN_MOVE, move, (moveIndex: integer) => {
|
||||
if (moveIndex === 4) {
|
||||
this.scene.ui.setMode(this.messageMode).then(() => this.rejectMoveAndEnd(move, pokemon));
|
||||
return;
|
||||
}
|
||||
const forgetSuccessText = i18next.t("battle:learnMoveForgetSuccess", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: pokemon.moveset[moveIndex]!.getName() });
|
||||
const fullText = [i18next.t("battle:countdownPoof"), forgetSuccessText, i18next.t("battle:learnMoveAnd")].join("$");
|
||||
this.scene.ui.setMode(this.messageMode).then(() => this.learnMove(moveIndex, move, pokemon, fullText));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This asks the player if they wish to end the current move learning process.
|
||||
*
|
||||
* > Stop trying to teach [MoveName]? --> `Mode.CONFIRM` --> Yes: > [Pokemon] did not learn the move [MoveName], No: `this.replaceMoveCheck()`
|
||||
*
|
||||
* If the player wishes to not teach the Pokemon the move, it displays a message and ends the phase.
|
||||
* If the player reconsiders, it repeats the process for a Pokemon with a full moveset once again.
|
||||
* @param move The Move to be learned
|
||||
* @param Pokemon The Pokemon learning the move
|
||||
*/
|
||||
async rejectMoveAndEnd(move: Move, pokemon: Pokemon) {
|
||||
await this.scene.ui.showTextPromise(i18next.t("battle:learnMoveStopTeaching", { moveName: move.name }), undefined, false);
|
||||
this.scene.ui.setModeWithoutClear(Mode.CONFIRM,
|
||||
() => {
|
||||
this.scene.ui.setMode(this.messageMode);
|
||||
this.scene.ui.showTextPromise(i18next.t("battle:learnMoveNotLearned", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name }), undefined, true).then(() => this.end());
|
||||
},
|
||||
() => {
|
||||
this.scene.ui.setMode(this.messageMode);
|
||||
this.replaceMoveCheck(move, pokemon);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This teaches the Pokemon the new move and ends the phase.
|
||||
* When a Pokemon forgets a move and learns a new one, its 'Learn Move' message is significantly longer.
|
||||
*
|
||||
* Pokemon with a `moveset.length < 4`
|
||||
* > [Pokemon] learned [MoveName]
|
||||
*
|
||||
* Pokemon with a `moveset.length > 4`
|
||||
* > 1... 2... and 3... and Poof!
|
||||
* > [Pokemon] forgot how to use [MoveName]
|
||||
* > And...
|
||||
* > [Pokemon] learned [MoveName]!
|
||||
* @param move The Move to be learned
|
||||
* @param Pokemon The Pokemon learning the move
|
||||
*/
|
||||
async learnMove(index: number, move: Move, pokemon: Pokemon, textMessage?: string) {
|
||||
if (this.fromTM) {
|
||||
pokemon.usedTMs.push(this.moveId);
|
||||
}
|
||||
pokemon.setMove(index, this.moveId);
|
||||
initMoveAnim(this.scene, this.moveId).then(() => {
|
||||
loadMoveAnimAssets(this.scene, [this.moveId], true);
|
||||
this.scene.playSound("level_up_fanfare"); // Sound loaded into game as is
|
||||
});
|
||||
this.scene.ui.setMode(this.messageMode);
|
||||
const learnMoveText = i18next.t("battle:learnMove", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name });
|
||||
textMessage = textMessage ? textMessage+"$"+learnMoveText : learnMoveText;
|
||||
await this.scene.ui.showTextPromise(textMessage, this.messageMode === Mode.EVOLUTION_SCENE ? 1000 : undefined, true);
|
||||
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeMoveLearnedTrigger, true);
|
||||
this.end();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import Phaser from "phaser";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { Species } from "#enums/species";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { LearnMovePhase } from "#app/phases/learn-move-phase";
|
||||
|
||||
describe("Learn Move Phase", () => {
|
||||
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.xpMultiplier(50);
|
||||
});
|
||||
|
||||
it("If Pokemon has less than 4 moves, its newest move will be added to the lowest empty index", async () => {
|
||||
game.override.moveset([Moves.SPLASH]);
|
||||
await game.startBattle([Species.BULBASAUR]);
|
||||
const pokemon = game.scene.getPlayerPokemon()!;
|
||||
const newMovePos = pokemon?.getMoveset().length;
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.doKillOpponents();
|
||||
await game.phaseInterceptor.to(LearnMovePhase);
|
||||
const levelMove = pokemon.getLevelMoves(5)[0];
|
||||
const levelReq = levelMove[0];
|
||||
const levelMoveId = levelMove[1];
|
||||
expect(pokemon.level).toBeGreaterThanOrEqual(levelReq);
|
||||
expect(pokemon?.getMoveset()[newMovePos]?.moveId).toBe(levelMoveId);
|
||||
});
|
||||
|
||||
/**
|
||||
* Future Tests:
|
||||
* If a Pokemon has four moves, the user can specify an old move to be forgotten and a new move will take its place.
|
||||
* If a Pokemon has four moves, the user can reject the new move, keeping the moveset the same.
|
||||
*/
|
||||
});
|
|
@ -48,6 +48,17 @@ export class OverridesHelper extends GameManagerHelper {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the XP Multiplier
|
||||
* @param value the XP multiplier to set
|
||||
* @returns `this`
|
||||
*/
|
||||
xpMultiplier(value: number): this {
|
||||
vi.spyOn(Overrides, "XP_MULTIPLIER_OVERRIDE", "get").mockReturnValue(value);
|
||||
this.log(`XP Multiplier set to ${value}!`);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the player (pokemon) starting held items
|
||||
* @param items the items to hold
|
||||
|
|
|
@ -12,6 +12,7 @@ import { EndEvolutionPhase } from "#app/phases/end-evolution-phase";
|
|||
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
|
||||
import { EvolutionPhase } from "#app/phases/evolution-phase";
|
||||
import { FaintPhase } from "#app/phases/faint-phase";
|
||||
import { LearnMovePhase } from "#app/phases/learn-move-phase";
|
||||
import { LevelCapPhase } from "#app/phases/level-cap-phase";
|
||||
import { LoginPhase } from "#app/phases/login-phase";
|
||||
import { MessagePhase } from "#app/phases/message-phase";
|
||||
|
@ -89,6 +90,7 @@ export default class PhaseInterceptor {
|
|||
[NextEncounterPhase, this.startPhase],
|
||||
[NewBattlePhase, this.startPhase],
|
||||
[VictoryPhase, this.startPhase],
|
||||
[LearnMovePhase, this.startPhase],
|
||||
[MoveEndPhase, this.startPhase],
|
||||
[StatStageChangePhase, this.startPhase],
|
||||
[ShinySparklePhase, this.startPhase],
|
||||
|
|
|
@ -289,6 +289,12 @@ export default class UI extends Phaser.GameObjects.Container {
|
|||
return handler.processInput(button);
|
||||
}
|
||||
|
||||
showTextPromise(text: string, callbackDelay: number = 0, prompt: boolean = true, promptDelay?: integer | null): Promise<void> {
|
||||
return new Promise<void>(resolve => {
|
||||
this.showText(text ?? "", null, () => resolve(), callbackDelay, prompt, promptDelay);
|
||||
});
|
||||
}
|
||||
|
||||
showText(text: string, delay?: integer | null, callback?: Function | null, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null): void {
|
||||
if (prompt && text.indexOf("$") > -1) {
|
||||
const messagePages = text.split(/\$/g).map(m => m.trim());
|
||||
|
|
Loading…
Reference in New Issue