[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:
Mumble 2024-09-09 09:11:46 -07:00 committed by GitHub
parent f5bf766ff7
commit 26eb63cf67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 186 additions and 78 deletions

View File

@ -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();
}
}

View File

@ -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.
*/
});

View File

@ -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

View File

@ -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],

View File

@ -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());