mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-25 08:49:32 +02:00
Compare commits
12 Commits
1839747a66
...
0172ecaff8
Author | SHA1 | Date | |
---|---|---|---|
|
0172ecaff8 | ||
|
076ef81691 | ||
|
23271901cf | ||
|
1517e0512e | ||
|
59f9c3e68d | ||
|
8f9edf38e8 | ||
|
0d0f8e3be3 | ||
|
938a4cf451 | ||
|
989cfbc223 | ||
|
63da1126a7 | ||
|
b5b44ea0b1 | ||
|
56b5461c95 |
@ -90,9 +90,13 @@ If this feature requires new text, the text should be integrated into the code w
|
||||
- For any feature pulled from the mainline Pokémon games (e.g. a Move or Ability implementation), it's best practice to include a source link for any added text.
|
||||
[Poké Corpus](https://abcboy101.github.io/poke-corpus/) is a great resource for finding text from the mainline games; otherwise, a video/picture showing the text being displayed should suffice.
|
||||
- You should also [notify the current Head of Translation](#notifying-translation) to ensure a fast response.
|
||||
3. At this point, you may begin [testing locales integration in your main PR](#documenting-locales-changes).
|
||||
4. The Translation Team will approve the locale PR (after corrections, if necessary), then merge it into `pokerogue-locales`.
|
||||
5. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment.
|
||||
3. Your locales should use the following format:
|
||||
- File names should be in `kebab-case`. Example: `trainer-names.json`
|
||||
- Key names should be in `camelCase`. Example: `aceTrainer`
|
||||
- If you make use of i18next's inbuilt [context support](https://www.i18next.com/translation-function/context), you need to use `snake_case` for the context key. Example: `aceTrainer_male`
|
||||
4. At this point, you may begin [testing locales integration in your main PR](#documenting-locales-changes).
|
||||
5. The Translation Team will approve the locale PR (after corrections, if necessary), then merge it into `pokerogue-locales`.
|
||||
6. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment.
|
||||
|
||||
[^2]: For those wondering, the reason for choosing English specifically is due to it being the master language set in Pontoon (the program used by the Translation Team to perform locale updates).
|
||||
If a key is present in any language _except_ the master language, it won't appear anywhere else in the translation tool, rendering missing English keys quite a hassle.
|
||||
|
@ -38,6 +38,7 @@ export enum UiMode {
|
||||
UNAVAILABLE,
|
||||
CHALLENGE_SELECT,
|
||||
RENAME_POKEMON,
|
||||
RENAME_RUN,
|
||||
RUN_HISTORY,
|
||||
RUN_INFO,
|
||||
TEST_DIALOGUE,
|
||||
|
@ -4,6 +4,7 @@ import { initEggMoves } from "#balance/egg-moves";
|
||||
import { initPokemonPrevolutions, initPokemonStarters } from "#balance/pokemon-evolutions";
|
||||
import { initSpecies } from "#balance/pokemon-species";
|
||||
import { initChallenges } from "#data/challenge";
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import { initTrainerTypeDialogue } from "#data/dialogue";
|
||||
import { initPokemonForms } from "#data/pokemon-forms";
|
||||
import { initModifierPools } from "#modifiers/init-modifier-pools";
|
||||
@ -16,6 +17,9 @@ import { initStatsKeys } from "#ui/game-stats-ui-handler";
|
||||
|
||||
/** Initialize the game. */
|
||||
export function initializeGame() {
|
||||
if (allMoves.length > 0) {
|
||||
return;
|
||||
}
|
||||
initModifierTypes();
|
||||
initModifierPools();
|
||||
initAchievements();
|
||||
|
@ -118,7 +118,10 @@ export class MovePhase extends BattlePhase {
|
||||
public start(): void {
|
||||
super.start();
|
||||
|
||||
console.log(MoveId[this.move.moveId], enumValueToKey(MoveUseMode, this.useMode));
|
||||
console.log(
|
||||
`%cMove: ${MoveId[this.move.moveId]}; Use Mode: ${enumValueToKey(MoveUseMode, this.useMode)}`,
|
||||
"color:RebeccaPurple",
|
||||
);
|
||||
|
||||
// Check if move is unusable (e.g. running out of PP due to a mid-turn Spite
|
||||
// or the user no longer being on field), ending the phase early if not.
|
||||
|
@ -127,6 +127,7 @@ export interface SessionSaveData {
|
||||
battleType: BattleType;
|
||||
trainer: TrainerData;
|
||||
gameVersion: string;
|
||||
runNameText: string;
|
||||
timestamp: number;
|
||||
challenges: ChallengeData[];
|
||||
mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME,
|
||||
@ -979,6 +980,54 @@ export class GameData {
|
||||
});
|
||||
}
|
||||
|
||||
async renameSession(slotId: number, newName: string): Promise<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
if (slotId < 0) {
|
||||
return resolve(false);
|
||||
}
|
||||
const sessionData: SessionSaveData | null = await this.getSession(slotId);
|
||||
|
||||
if (!sessionData) {
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
if (newName === "") {
|
||||
return resolve(true);
|
||||
}
|
||||
|
||||
sessionData.runNameText = newName;
|
||||
const updatedDataStr = JSON.stringify(sessionData);
|
||||
const encrypted = encrypt(updatedDataStr, bypassLogin);
|
||||
const secretId = this.secretId;
|
||||
const trainerId = this.trainerId;
|
||||
|
||||
if (bypassLogin) {
|
||||
localStorage.setItem(
|
||||
`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`,
|
||||
encrypt(updatedDataStr, bypassLogin),
|
||||
);
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
pokerogueApi.savedata.session
|
||||
.update({ slot: slotId, trainerId, secretId, clientSessionId }, encrypted)
|
||||
.then(error => {
|
||||
if (error) {
|
||||
console.error("Failed to update session name:", error);
|
||||
resolve(false);
|
||||
} else {
|
||||
localStorage.setItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, encrypted);
|
||||
updateUserInfo().then(success => {
|
||||
if (success !== null && !success) {
|
||||
return resolve(false);
|
||||
}
|
||||
});
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
|
||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
54
src/ui/rename-run-ui-handler.ts
Normal file
54
src/ui/rename-run-ui-handler.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import i18next from "i18next";
|
||||
import type { InputFieldConfig } from "./form-modal-ui-handler";
|
||||
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
||||
import type { ModalConfig } from "./modal-ui-handler";
|
||||
|
||||
export class RenameRunFormUiHandler extends FormModalUiHandler {
|
||||
getModalTitle(_config?: ModalConfig): string {
|
||||
return i18next.t("menu:renamerun");
|
||||
}
|
||||
|
||||
getWidth(_config?: ModalConfig): number {
|
||||
return 160;
|
||||
}
|
||||
|
||||
getMargin(_config?: ModalConfig): [number, number, number, number] {
|
||||
return [0, 0, 48, 0];
|
||||
}
|
||||
|
||||
getButtonLabels(_config?: ModalConfig): string[] {
|
||||
return [i18next.t("menu:rename"), i18next.t("menu:cancel")];
|
||||
}
|
||||
|
||||
getReadableErrorMessage(error: string): string {
|
||||
const colonIndex = error?.indexOf(":");
|
||||
if (colonIndex > 0) {
|
||||
error = error.slice(0, colonIndex);
|
||||
}
|
||||
|
||||
return super.getReadableErrorMessage(error);
|
||||
}
|
||||
|
||||
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||
return [{ label: i18next.t("menu:runName") }];
|
||||
}
|
||||
|
||||
show(args: any[]): boolean {
|
||||
if (!super.show(args)) {
|
||||
return false;
|
||||
}
|
||||
if (this.inputs?.length) {
|
||||
this.inputs.forEach(input => {
|
||||
input.text = "";
|
||||
});
|
||||
}
|
||||
const config = args[0] as ModalConfig;
|
||||
this.submitAction = _ => {
|
||||
this.sanitizeInputs();
|
||||
const sanitizedName = btoa(encodeURIComponent(this.inputs[0].text));
|
||||
config.buttonActions[0](sanitizedName);
|
||||
return true;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
@ -207,6 +207,10 @@ export class RunInfoUiHandler extends UiHandler {
|
||||
headerText.setOrigin(0, 0);
|
||||
headerText.setPositionRelative(headerBg, 8, 4);
|
||||
this.runContainer.add(headerText);
|
||||
const runName = addTextObject(0, 0, this.runInfo.runNameText, TextStyle.WINDOW);
|
||||
runName.setOrigin(0, 0);
|
||||
runName.setPositionRelative(headerBg, 60, 4);
|
||||
this.runContainer.add(runName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { GameMode } from "#app/game-mode";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { Button } from "#enums/buttons";
|
||||
import { GameModes } from "#enums/game-modes";
|
||||
import { TextStyle } from "#enums/text-style";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
// biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts`
|
||||
import * as Modifier from "#modifiers/modifier";
|
||||
import type { SessionSaveData } from "#system/game-data";
|
||||
import type { PokemonData } from "#system/pokemon-data";
|
||||
import type { OptionSelectConfig } from "#ui/abstract-option-select-ui-handler";
|
||||
import { MessageUiHandler } from "#ui/message-ui-handler";
|
||||
import { RunDisplayMode } from "#ui/run-info-ui-handler";
|
||||
import { addTextObject } from "#ui/text";
|
||||
@ -15,7 +17,7 @@ import { fixedInt, formatLargeNumber, getPlayTimeString, isNullOrUndefined } fro
|
||||
import i18next from "i18next";
|
||||
|
||||
const SESSION_SLOTS_COUNT = 5;
|
||||
const SLOTS_ON_SCREEN = 3;
|
||||
const SLOTS_ON_SCREEN = 2;
|
||||
|
||||
export enum SaveSlotUiMode {
|
||||
LOAD,
|
||||
@ -33,6 +35,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
|
||||
private uiMode: SaveSlotUiMode;
|
||||
private saveSlotSelectCallback: SaveSlotSelectCallback | null;
|
||||
protected manageDataConfig: OptionSelectConfig;
|
||||
|
||||
private scrollCursor = 0;
|
||||
|
||||
@ -101,6 +104,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
|
||||
processInput(button: Button): boolean {
|
||||
const ui = this.getUi();
|
||||
const manageDataOptions: any[] = [];
|
||||
|
||||
let success = false;
|
||||
let error = false;
|
||||
@ -109,14 +113,115 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
const originalCallback = this.saveSlotSelectCallback;
|
||||
if (button === Button.ACTION) {
|
||||
const cursor = this.cursor + this.scrollCursor;
|
||||
if (this.uiMode === SaveSlotUiMode.LOAD && !this.sessionSlots[cursor].hasData) {
|
||||
const sessionSlot = this.sessionSlots[cursor];
|
||||
if (this.uiMode === SaveSlotUiMode.LOAD && !sessionSlot.hasData) {
|
||||
error = true;
|
||||
} else {
|
||||
switch (this.uiMode) {
|
||||
case SaveSlotUiMode.LOAD:
|
||||
this.saveSlotSelectCallback = null;
|
||||
originalCallback?.(cursor);
|
||||
if (!sessionSlot.malformed) {
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("menu:loadGame"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
originalCallback?.(cursor);
|
||||
return true;
|
||||
},
|
||||
keepOpen: false,
|
||||
});
|
||||
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("saveSlotSelectUiHandler:renameRun"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
ui.setOverlayMode(
|
||||
UiMode.RENAME_RUN,
|
||||
{
|
||||
buttonActions: [
|
||||
(sanitizedName: string) => {
|
||||
const name = decodeURIComponent(atob(sanitizedName));
|
||||
globalScene.gameData.renameSession(cursor, name).then(response => {
|
||||
if (response[0] === false) {
|
||||
globalScene.reset(true);
|
||||
} else {
|
||||
this.clearSessionSlots();
|
||||
this.cursorObj = null;
|
||||
this.populateSessionSlots();
|
||||
this.setScrollCursor(0);
|
||||
this.setCursor(0);
|
||||
ui.revertMode();
|
||||
ui.showText("", 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
() => {
|
||||
ui.revertMode();
|
||||
},
|
||||
],
|
||||
},
|
||||
"",
|
||||
);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.manageDataConfig = {
|
||||
xOffset: 0,
|
||||
yOffset: 48,
|
||||
options: manageDataOptions,
|
||||
maxOptions: 4,
|
||||
};
|
||||
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("saveSlotSelectUiHandler:deleteRun"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
ui.showText(i18next.t("saveSlotSelectUiHandler:deleteData"), null, () => {
|
||||
ui.setOverlayMode(
|
||||
UiMode.CONFIRM,
|
||||
() => {
|
||||
globalScene.gameData.tryClearSession(cursor).then(response => {
|
||||
if (response[0] === false) {
|
||||
globalScene.reset(true);
|
||||
} else {
|
||||
this.clearSessionSlots();
|
||||
this.cursorObj = null;
|
||||
this.populateSessionSlots();
|
||||
this.setScrollCursor(0);
|
||||
this.setCursor(0);
|
||||
ui.revertMode();
|
||||
ui.showText("", 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
() => {
|
||||
ui.revertMode();
|
||||
ui.showText("", 0);
|
||||
},
|
||||
false,
|
||||
0,
|
||||
19,
|
||||
import.meta.env.DEV ? 300 : 2000,
|
||||
);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
keepOpen: false,
|
||||
});
|
||||
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("menuUiHandler:cancel"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
return true;
|
||||
},
|
||||
keepOpen: true,
|
||||
});
|
||||
|
||||
ui.setOverlayMode(UiMode.MENU_OPTION_SELECT, this.manageDataConfig);
|
||||
break;
|
||||
|
||||
case SaveSlotUiMode.SAVE: {
|
||||
const saveAndCallback = () => {
|
||||
const originalCallback = this.saveSlotSelectCallback;
|
||||
@ -161,6 +266,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
}
|
||||
} else {
|
||||
this.saveSlotSelectCallback = null;
|
||||
ui.showText("", 0);
|
||||
originalCallback?.(-1);
|
||||
success = true;
|
||||
}
|
||||
@ -267,33 +373,34 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
this.cursorObj = globalScene.add.container(0, 0);
|
||||
const cursorBox = globalScene.add.nineslice(
|
||||
0,
|
||||
0,
|
||||
15,
|
||||
"select_cursor_highlight_thick",
|
||||
undefined,
|
||||
296,
|
||||
44,
|
||||
294,
|
||||
this.sessionSlots[prevSlotIndex ?? 0]?.saveData?.runNameText ? 50 : 60,
|
||||
6,
|
||||
6,
|
||||
6,
|
||||
6,
|
||||
);
|
||||
const rightArrow = globalScene.add.image(0, 0, "cursor");
|
||||
rightArrow.setPosition(160, 0);
|
||||
rightArrow.setPosition(160, 15);
|
||||
rightArrow.setName("rightArrow");
|
||||
this.cursorObj.add([cursorBox, rightArrow]);
|
||||
this.sessionSlotsContainer.add(this.cursorObj);
|
||||
}
|
||||
const cursorPosition = cursor + this.scrollCursor;
|
||||
const cursorIncrement = cursorPosition * 56;
|
||||
const cursorIncrement = cursorPosition * 76;
|
||||
if (this.sessionSlots[cursorPosition] && this.cursorObj) {
|
||||
const hasData = this.sessionSlots[cursorPosition].hasData;
|
||||
const session = this.sessionSlots[cursorPosition];
|
||||
const hasData = session.hasData && !session.malformed;
|
||||
// If the session slot lacks session data, it does not move from its default, central position.
|
||||
// Only session slots with session data will move leftwards and have a visible arrow.
|
||||
if (!hasData) {
|
||||
this.cursorObj.setPosition(151, 26 + cursorIncrement);
|
||||
this.cursorObj.setPosition(151, 20 + cursorIncrement);
|
||||
this.sessionSlots[cursorPosition].setPosition(0, cursorIncrement);
|
||||
} else {
|
||||
this.cursorObj.setPosition(145, 26 + cursorIncrement);
|
||||
this.cursorObj.setPosition(145, 20 + cursorIncrement);
|
||||
this.sessionSlots[cursorPosition].setPosition(-6, cursorIncrement);
|
||||
}
|
||||
this.setArrowVisibility(hasData);
|
||||
@ -311,7 +418,8 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
revertSessionSlot(slotIndex: number): void {
|
||||
const sessionSlot = this.sessionSlots[slotIndex];
|
||||
if (sessionSlot) {
|
||||
sessionSlot.setPosition(0, slotIndex * 56);
|
||||
const valueHeight = 76;
|
||||
sessionSlot.setPosition(0, slotIndex * valueHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@ -340,7 +448,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
this.setCursor(this.cursor, prevSlotIndex);
|
||||
globalScene.tweens.add({
|
||||
targets: this.sessionSlotsContainer,
|
||||
y: this.sessionSlotsContainerInitialY - 56 * scrollCursor,
|
||||
y: this.sessionSlotsContainerInitialY - 76 * scrollCursor,
|
||||
duration: fixedInt(325),
|
||||
ease: "Sine.easeInOut",
|
||||
});
|
||||
@ -374,12 +482,14 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
class SessionSlot extends Phaser.GameObjects.Container {
|
||||
public slotId: number;
|
||||
public hasData: boolean;
|
||||
/** Indicates the save slot ran into an error while being loaded */
|
||||
public malformed: boolean;
|
||||
private slotWindow: Phaser.GameObjects.NineSlice;
|
||||
private loadingLabel: Phaser.GameObjects.Text;
|
||||
|
||||
public saveData: SessionSaveData;
|
||||
|
||||
constructor(slotId: number) {
|
||||
super(globalScene, 0, slotId * 56);
|
||||
super(globalScene, 0, slotId * 76);
|
||||
|
||||
this.slotId = slotId;
|
||||
|
||||
@ -387,32 +497,89 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
setup() {
|
||||
const slotWindow = addWindow(0, 0, 304, 52);
|
||||
this.add(slotWindow);
|
||||
this.slotWindow = addWindow(0, 0, 304, 70);
|
||||
this.add(this.slotWindow);
|
||||
|
||||
this.loadingLabel = addTextObject(152, 26, i18next.t("saveSlotSelectUiHandler:loading"), TextStyle.WINDOW);
|
||||
this.loadingLabel = addTextObject(152, 33, i18next.t("saveSlotSelectUiHandler:loading"), TextStyle.WINDOW);
|
||||
this.loadingLabel.setOrigin(0.5, 0.5);
|
||||
this.add(this.loadingLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a name for sessions that don't have a name yet.
|
||||
* @param data - The {@linkcode SessionSaveData} being checked
|
||||
* @returns The default name for the given data.
|
||||
*/
|
||||
decideFallback(data: SessionSaveData): string {
|
||||
let fallbackName = `${GameMode.getModeName(data.gameMode)}`;
|
||||
switch (data.gameMode) {
|
||||
case GameModes.CLASSIC:
|
||||
fallbackName += ` (${globalScene.gameData.gameStats.classicSessionsPlayed + 1})`;
|
||||
break;
|
||||
case GameModes.ENDLESS:
|
||||
case GameModes.SPLICED_ENDLESS:
|
||||
fallbackName += ` (${globalScene.gameData.gameStats.endlessSessionsPlayed + 1})`;
|
||||
break;
|
||||
case GameModes.DAILY: {
|
||||
const runDay = new Date(data.timestamp).toLocaleDateString();
|
||||
fallbackName += ` (${runDay})`;
|
||||
break;
|
||||
}
|
||||
case GameModes.CHALLENGE: {
|
||||
const activeChallenges = data.challenges.filter(c => c.value !== 0);
|
||||
if (activeChallenges.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
fallbackName = "";
|
||||
for (const challenge of activeChallenges.slice(0, 3)) {
|
||||
if (fallbackName !== "") {
|
||||
fallbackName += ", ";
|
||||
}
|
||||
fallbackName += challenge.toChallenge().getName();
|
||||
}
|
||||
|
||||
if (activeChallenges.length > 3) {
|
||||
fallbackName += ", ...";
|
||||
} else if (fallbackName === "") {
|
||||
// Something went wrong when retrieving the names of the active challenges,
|
||||
// so fall back to just naming the run "Challenge"
|
||||
fallbackName = `${GameMode.getModeName(data.gameMode)}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return fallbackName;
|
||||
}
|
||||
|
||||
async setupWithData(data: SessionSaveData) {
|
||||
const hasName = data?.runNameText;
|
||||
this.remove(this.loadingLabel, true);
|
||||
if (hasName) {
|
||||
const nameLabel = addTextObject(8, 5, data.runNameText, TextStyle.WINDOW);
|
||||
this.add(nameLabel);
|
||||
} else {
|
||||
const fallbackName = this.decideFallback(data);
|
||||
await globalScene.gameData.renameSession(this.slotId, fallbackName);
|
||||
const nameLabel = addTextObject(8, 5, fallbackName, TextStyle.WINDOW);
|
||||
this.add(nameLabel);
|
||||
}
|
||||
|
||||
const gameModeLabel = addTextObject(
|
||||
8,
|
||||
5,
|
||||
19,
|
||||
`${GameMode.getModeName(data.gameMode) || i18next.t("gameMode:unknown")} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${data.waveIndex}`,
|
||||
TextStyle.WINDOW,
|
||||
);
|
||||
this.add(gameModeLabel);
|
||||
|
||||
const timestampLabel = addTextObject(8, 19, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW);
|
||||
const timestampLabel = addTextObject(8, 33, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW);
|
||||
this.add(timestampLabel);
|
||||
|
||||
const playTimeLabel = addTextObject(8, 33, getPlayTimeString(data.playTime), TextStyle.WINDOW);
|
||||
const playTimeLabel = addTextObject(8, 47, getPlayTimeString(data.playTime), TextStyle.WINDOW);
|
||||
this.add(playTimeLabel);
|
||||
|
||||
const pokemonIconsContainer = globalScene.add.container(144, 4);
|
||||
const pokemonIconsContainer = globalScene.add.container(144, 16);
|
||||
data.party.forEach((p: PokemonData, i: number) => {
|
||||
const iconContainer = globalScene.add.container(26 * i, 0);
|
||||
iconContainer.setScale(0.75);
|
||||
@ -427,13 +594,9 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
||||
TextStyle.PARTY,
|
||||
{ fontSize: "54px", color: "#f8f8f8" },
|
||||
);
|
||||
text.setShadow(0, 0, undefined);
|
||||
text.setStroke("#424242", 14);
|
||||
text.setOrigin(1, 0);
|
||||
|
||||
iconContainer.add(icon);
|
||||
iconContainer.add(text);
|
||||
text.setShadow(0, 0, undefined).setStroke("#424242", 14).setOrigin(1, 0);
|
||||
|
||||
iconContainer.add([icon, text]);
|
||||
pokemonIconsContainer.add(iconContainer);
|
||||
|
||||
pokemon.destroy();
|
||||
@ -441,7 +604,7 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
||||
|
||||
this.add(pokemonIconsContainer);
|
||||
|
||||
const modifierIconsContainer = globalScene.add.container(148, 30);
|
||||
const modifierIconsContainer = globalScene.add.container(148, 38);
|
||||
modifierIconsContainer.setScale(0.5);
|
||||
let visibleModifierIndex = 0;
|
||||
for (const m of data.modifiers) {
|
||||
@ -464,22 +627,33 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
||||
|
||||
load(): Promise<boolean> {
|
||||
return new Promise<boolean>(resolve => {
|
||||
globalScene.gameData.getSession(this.slotId).then(async sessionData => {
|
||||
// Ignore the results if the view was exited
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
if (!sessionData) {
|
||||
this.hasData = false;
|
||||
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
this.hasData = true;
|
||||
this.saveData = sessionData;
|
||||
await this.setupWithData(sessionData);
|
||||
resolve(true);
|
||||
});
|
||||
globalScene.gameData
|
||||
.getSession(this.slotId)
|
||||
.then(async sessionData => {
|
||||
// Ignore the results if the view was exited
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
this.hasData = !!sessionData;
|
||||
if (!sessionData) {
|
||||
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
this.saveData = sessionData;
|
||||
this.setupWithData(sessionData);
|
||||
resolve(true);
|
||||
})
|
||||
.catch(e => {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
console.warn(`Failed to load session slot #${this.slotId}:`, e);
|
||||
this.loadingLabel.setText(i18next.t("menu:failedToLoadSession"));
|
||||
this.hasData = true;
|
||||
this.malformed = true;
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ import { addWindow } from "#ui/ui-theme";
|
||||
import { UnavailableModalUiHandler } from "#ui/unavailable-modal-ui-handler";
|
||||
import { executeIf } from "#utils/common";
|
||||
import i18next from "i18next";
|
||||
import { RenameRunFormUiHandler } from "./rename-run-ui-handler";
|
||||
|
||||
const transitionModes = [
|
||||
UiMode.SAVE_SLOT,
|
||||
@ -98,6 +99,7 @@ const noTransitionModes = [
|
||||
UiMode.SESSION_RELOAD,
|
||||
UiMode.UNAVAILABLE,
|
||||
UiMode.RENAME_POKEMON,
|
||||
UiMode.RENAME_RUN,
|
||||
UiMode.TEST_DIALOGUE,
|
||||
UiMode.AUTO_COMPLETE,
|
||||
UiMode.ADMIN,
|
||||
@ -168,6 +170,7 @@ export class UI extends Phaser.GameObjects.Container {
|
||||
new UnavailableModalUiHandler(),
|
||||
new GameChallengesUiHandler(),
|
||||
new RenameFormUiHandler(),
|
||||
new RenameRunFormUiHandler(),
|
||||
new RunHistoryUiHandler(),
|
||||
new RunInfoUiHandler(),
|
||||
new TestDialogueUiHandler(UiMode.TEST_DIALOGUE),
|
||||
|
@ -99,7 +99,7 @@ describe("Abilities - Cud Chew", () => {
|
||||
expect(abDisplaySpy.mock.calls[1][2]).toBe(false);
|
||||
|
||||
// should display messgae
|
||||
expect(game.textInterceptor.getLatestMessage()).toBe(
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("battle:hpIsFull", {
|
||||
pokemonName: getPokemonNameWithAffix(farigiraf),
|
||||
}),
|
||||
|
@ -9,7 +9,7 @@ import { TurnStartPhase } from "#phases/turn-start-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import i18next from "i18next";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Focus Punch", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -125,8 +125,8 @@ describe("Moves - Focus Punch", () => {
|
||||
game.move.select(MoveId.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", { pokemonName: "Charizard" }));
|
||||
expect(game.textInterceptor.logs).toContain(i18next.t("moveTriggers:lostFocus", { pokemonName: "Charizard" }));
|
||||
expect(game.textInterceptor.logs).not.toContain(i18next.t("battle:attackFailed"));
|
||||
});
|
||||
});
|
||||
|
82
test/system/rename-run.test.ts
Normal file
82
test/system/rename-run.test.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import * as account from "#app/account";
|
||||
import * as bypassLoginModule from "#app/global-vars/bypass-login";
|
||||
import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
|
||||
import type { SessionSaveData } from "#app/system/game-data";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("System - Rename Run", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.battleStyle("single")
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
describe("renameSession", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(bypassLoginModule, "bypassLogin", "get").mockReturnValue(false);
|
||||
vi.spyOn(account, "updateUserInfo").mockImplementation(async () => [true, 1]);
|
||||
});
|
||||
|
||||
it("should return false if slotId < 0", async () => {
|
||||
const result = await game.scene.gameData.renameSession(-1, "Named Run");
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return false if getSession returns null", async () => {
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue(null as unknown as SessionSaveData);
|
||||
|
||||
const result = await game.scene.gameData.renameSession(-1, "Named Run");
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true if bypassLogin is true", async () => {
|
||||
vi.spyOn(bypassLoginModule, "bypassLogin", "get").mockReturnValue(true);
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue({} as SessionSaveData);
|
||||
|
||||
const result = await game.scene.gameData.renameSession(0, "Named Run");
|
||||
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false if api returns error", async () => {
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue({} as SessionSaveData);
|
||||
vi.spyOn(pokerogueApi.savedata.session, "update").mockResolvedValue("Unknown Error!");
|
||||
|
||||
const result = await game.scene.gameData.renameSession(0, "Named Run");
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true if api is succesfull", async () => {
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue({} as SessionSaveData);
|
||||
vi.spyOn(pokerogueApi.savedata.session, "update").mockResolvedValue("");
|
||||
|
||||
const result = await game.scene.gameData.renameSession(0, "Named Run");
|
||||
|
||||
expect(result).toEqual(true);
|
||||
expect(account.updateUserInfo).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
@ -6,17 +6,13 @@ import * as bypassLoginModule from "#app/global-vars/bypass-login";
|
||||
import { MoveAnim } from "#data/battle-anims";
|
||||
import { Pokemon } from "#field/pokemon";
|
||||
import { version } from "#package.json";
|
||||
import { blobToString } from "#test/test-utils/game-manager-utils";
|
||||
import { MockClock } from "#test/test-utils/mocks/mock-clock";
|
||||
import { MockFetch } from "#test/test-utils/mocks/mock-fetch";
|
||||
import { MockGameObjectCreator } from "#test/test-utils/mocks/mock-game-object-creator";
|
||||
import { MockLoader } from "#test/test-utils/mocks/mock-loader";
|
||||
import { MockTextureManager } from "#test/test-utils/mocks/mock-texture-manager";
|
||||
import { MockTimedEventManager } from "#test/test-utils/mocks/mock-timed-event-manager";
|
||||
import { MockContainer } from "#test/test-utils/mocks/mocks-container/mock-container";
|
||||
import { PokedexMonContainer } from "#ui/pokedex-mon-container";
|
||||
import { sessionIdKey } from "#utils/common";
|
||||
import { setCookie } from "#utils/cookies";
|
||||
import fs from "node:fs";
|
||||
import Phaser from "phaser";
|
||||
import { vi } from "vitest";
|
||||
@ -28,20 +24,6 @@ const GamepadPlugin = Phaser.Input.Gamepad.GamepadPlugin;
|
||||
const EventEmitter = Phaser.Events.EventEmitter;
|
||||
const UpdateList = Phaser.GameObjects.UpdateList;
|
||||
|
||||
window.URL.createObjectURL = (blob: Blob) => {
|
||||
blobToString(blob).then((data: string) => {
|
||||
localStorage.setItem("toExport", data);
|
||||
});
|
||||
return null;
|
||||
};
|
||||
navigator.getGamepads = () => [];
|
||||
global.fetch = vi.fn(MockFetch);
|
||||
setCookie(sessionIdKey, "fake_token");
|
||||
|
||||
window.matchMedia = () => ({
|
||||
matches: false,
|
||||
});
|
||||
|
||||
export class GameWrapper {
|
||||
public game: Phaser.Game;
|
||||
public scene: BattleScene;
|
||||
@ -99,6 +81,7 @@ export class GameWrapper {
|
||||
removeAll: () => null,
|
||||
};
|
||||
|
||||
// TODO: Can't we just turn on `noAudio` in audio config?
|
||||
this.scene.sound = {
|
||||
play: () => null,
|
||||
pause: () => null,
|
||||
|
@ -19,6 +19,7 @@ import type { ModifierOverride } from "#modifiers/modifier-type";
|
||||
import type { Variant } from "#sprites/variant";
|
||||
import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper";
|
||||
import { coerceArray, shiftCharCodes } from "#utils/common";
|
||||
import chalk from "chalk";
|
||||
import { vi } from "vitest";
|
||||
|
||||
/**
|
||||
@ -665,6 +666,6 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
}
|
||||
|
||||
private log(...params: any[]) {
|
||||
console.log("Overrides:", ...params);
|
||||
console.log(chalk.hex("#b0b01eff")("Overrides:", ...params));
|
||||
}
|
||||
}
|
||||
|
@ -1,80 +0,0 @@
|
||||
const originalLog = console.log;
|
||||
const originalError = console.error;
|
||||
const originalDebug = console.debug;
|
||||
const originalWarn = console.warn;
|
||||
|
||||
const blacklist = ["Phaser", "variant icon does not exist", 'Texture "%s" not found'];
|
||||
const whitelist = ["Phase"];
|
||||
|
||||
export class MockConsoleLog {
|
||||
constructor(
|
||||
private logDisabled = false,
|
||||
private phaseText = false,
|
||||
) {}
|
||||
private logs: any[] = [];
|
||||
private notified: any[] = [];
|
||||
|
||||
public log(...args) {
|
||||
const argsStr = this.getStr(args);
|
||||
this.logs.push(argsStr);
|
||||
if (this.logDisabled && !this.phaseText) {
|
||||
return;
|
||||
}
|
||||
if ((this.phaseText && !whitelist.some(b => argsStr.includes(b))) || blacklist.some(b => argsStr.includes(b))) {
|
||||
return;
|
||||
}
|
||||
originalLog(args);
|
||||
}
|
||||
public error(...args) {
|
||||
const argsStr = this.getStr(args);
|
||||
this.logs.push(argsStr);
|
||||
originalError(args); // Appelle le console.error originel
|
||||
}
|
||||
public debug(...args) {
|
||||
const argsStr = this.getStr(args);
|
||||
this.logs.push(argsStr);
|
||||
if (this.logDisabled && !this.phaseText) {
|
||||
return;
|
||||
}
|
||||
if (!whitelist.some(b => argsStr.includes(b)) || blacklist.some(b => argsStr.includes(b))) {
|
||||
return;
|
||||
}
|
||||
originalDebug(args);
|
||||
}
|
||||
warn(...args) {
|
||||
const argsStr = this.getStr(args);
|
||||
this.logs.push(args);
|
||||
if (this.logDisabled && !this.phaseText) {
|
||||
return;
|
||||
}
|
||||
if (!whitelist.some(b => argsStr.includes(b)) || blacklist.some(b => argsStr.includes(b))) {
|
||||
return;
|
||||
}
|
||||
originalWarn(args);
|
||||
}
|
||||
notify(msg) {
|
||||
originalLog(msg);
|
||||
this.notified.push(msg);
|
||||
}
|
||||
getLogs() {
|
||||
return this.logs;
|
||||
}
|
||||
clearLogs() {
|
||||
this.logs = [];
|
||||
}
|
||||
getStr(...args) {
|
||||
return args
|
||||
.map(arg => {
|
||||
if (typeof arg === "object" && arg !== null) {
|
||||
// Handle objects including arrays
|
||||
return JSON.stringify(arg, (_key, value) => (typeof value === "bigint" ? value.toString() : value));
|
||||
}
|
||||
if (typeof arg === "bigint") {
|
||||
// Handle BigInt values
|
||||
return arg.toString();
|
||||
}
|
||||
return arg.toString();
|
||||
})
|
||||
.join(";");
|
||||
}
|
||||
}
|
150
test/test-utils/mocks/mock-console/color-map.json
Normal file
150
test/test-utils/mocks/mock-console/color-map.json
Normal file
@ -0,0 +1,150 @@
|
||||
{
|
||||
"AliceBlue": "f0f8ff",
|
||||
"AntiqueWhite": "faebd7",
|
||||
"Aqua": "00ffff",
|
||||
"Aquamarine": "7fffd4",
|
||||
"Azure": "f0ffff",
|
||||
"Beige": "f5f5dc",
|
||||
"Bisque": "ffe4c4",
|
||||
"Black": "000000",
|
||||
"BlanchedAlmond": "ffebcd",
|
||||
"Blue": "0000ff",
|
||||
"BlueViolet": "8a2be2",
|
||||
"Brown": "a52a2a",
|
||||
"BurlyWood": "deb887",
|
||||
"CadetBlue": "5f9ea0",
|
||||
"Chartreuse": "7fff00",
|
||||
"Chocolate": "d2691e",
|
||||
"Coral": "ff7f50",
|
||||
"CornflowerBlue": "6495ed",
|
||||
"Cornsilk": "fff8dc",
|
||||
"Crimson": "dc143c",
|
||||
"Cyan": "00ffff",
|
||||
"DarkBlue": "00008b",
|
||||
"DarkCyan": "008b8b",
|
||||
"DarkGoldenRod": "b8860b",
|
||||
"DarkGray": "a9a9a9",
|
||||
"DarkGrey": "a9a9a9",
|
||||
"DarkGreen": "006400",
|
||||
"DarkKhaki": "bdb76b",
|
||||
"DarkMagenta": "8b008b",
|
||||
"DarkOliveGreen": "556b2f",
|
||||
"DarkOrange": "ff8c00",
|
||||
"DarkOrchid": "9932cc",
|
||||
"DarkRed": "8b0000",
|
||||
"DarkSalmon": "e9967a",
|
||||
"DarkSeaGreen": "8fbc8f",
|
||||
"DarkSlateBlue": "483d8b",
|
||||
"DarkSlateGray": "2f4f4f",
|
||||
"DarkSlateGrey": "2f4f4f",
|
||||
"DarkTurquoise": "00ced1",
|
||||
"DarkViolet": "9400d3",
|
||||
"DeepPink": "ff1493",
|
||||
"DeepSkyBlue": "00bfff",
|
||||
"DimGray": "696969",
|
||||
"DimGrey": "696969",
|
||||
"DodgerBlue": "1e90ff",
|
||||
"FireBrick": "b22222",
|
||||
"FloralWhite": "fffaf0",
|
||||
"ForestGreen": "228b22",
|
||||
"Fuchsia": "ff00ff",
|
||||
"Gainsboro": "dcdcdc",
|
||||
"GhostWhite": "f8f8ff",
|
||||
"Gold": "ffd700",
|
||||
"GoldenRod": "daa520",
|
||||
"Gray": "808080",
|
||||
"Grey": "808080",
|
||||
"Green": "008000",
|
||||
"GreenYellow": "adff2f",
|
||||
"HoneyDew": "f0fff0",
|
||||
"HotPink": "ff69b4",
|
||||
"IndianRed": "cd5c5c",
|
||||
"Indigo": "4b0082",
|
||||
"Ivory": "fffff0",
|
||||
"Khaki": "f0e68c",
|
||||
"Lavender": "e6e6fa",
|
||||
"LavenderBlush": "fff0f5",
|
||||
"LawnGreen": "7cfc00",
|
||||
"LemonChiffon": "fffacd",
|
||||
"LightBlue": "add8e6",
|
||||
"LightCoral": "f08080",
|
||||
"LightCyan": "e0ffff",
|
||||
"LightGoldenRodYellow": "fafad2",
|
||||
"LightGray": "d3d3d3",
|
||||
"LightGrey": "d3d3d3",
|
||||
"LightGreen": "90ee90",
|
||||
"LightPink": "ffb6c1",
|
||||
"LightSalmon": "ffa07a",
|
||||
"LightSeaGreen": "20b2aa",
|
||||
"LightSkyBlue": "87cefa",
|
||||
"LightSlateGray": "778899",
|
||||
"LightSlateGrey": "778899",
|
||||
"LightSteelBlue": "b0c4de",
|
||||
"LightYellow": "ffffe0",
|
||||
"Lime": "00ff00",
|
||||
"LimeGreen": "32cd32",
|
||||
"Linen": "faf0e6",
|
||||
"Magenta": "ff00ff",
|
||||
"Maroon": "800000",
|
||||
"MediumAquaMarine": "66cdaa",
|
||||
"MediumBlue": "0000cd",
|
||||
"MediumOrchid": "ba55d3",
|
||||
"MediumPurple": "9370db",
|
||||
"MediumSeaGreen": "3cb371",
|
||||
"MediumSlateBlue": "7b68ee",
|
||||
"MediumSpringGreen": "00fa9a",
|
||||
"MediumTurquoise": "48d1cc",
|
||||
"MediumVioletRed": "c71585",
|
||||
"MidnightBlue": "191970",
|
||||
"MintCream": "f5fffa",
|
||||
"MistyRose": "ffe4e1",
|
||||
"Moccasin": "ffe4b5",
|
||||
"NavajoWhite": "ffdead",
|
||||
"Navy": "000080",
|
||||
"OldLace": "fdf5e6",
|
||||
"Olive": "808000",
|
||||
"OliveDrab": "6b8e23",
|
||||
"Orange": "ffa500",
|
||||
"OrangeRed": "ff4500",
|
||||
"Orchid": "da70d6",
|
||||
"PaleGoldenRod": "eee8aa",
|
||||
"PaleGreen": "98fb98",
|
||||
"PaleTurquoise": "afeeee",
|
||||
"PaleVioletRed": "db7093",
|
||||
"PapayaWhip": "ffefd5",
|
||||
"PeachPuff": "ffdab9",
|
||||
"Peru": "cd853f",
|
||||
"Pink": "ffc0cb",
|
||||
"Plum": "dda0dd",
|
||||
"PowderBlue": "b0e0e6",
|
||||
"Purple": "800080",
|
||||
"RebeccaPurple": "663399",
|
||||
"Red": "ff0000",
|
||||
"RosyBrown": "bc8f8f",
|
||||
"RoyalBlue": "4169e1",
|
||||
"SaddleBrown": "8b4513",
|
||||
"Salmon": "fa8072",
|
||||
"SandyBrown": "f4a460",
|
||||
"SeaGreen": "2e8b57",
|
||||
"SeaShell": "fff5ee",
|
||||
"Sienna": "a0522d",
|
||||
"Silver": "c0c0c0",
|
||||
"SkyBlue": "87ceeb",
|
||||
"SlateBlue": "6a5acd",
|
||||
"SlateGray": "708090",
|
||||
"SlateGrey": "708090",
|
||||
"Snow": "fffafa",
|
||||
"SpringGreen": "00ff7f",
|
||||
"SteelBlue": "4682b4",
|
||||
"Tan": "d2b48c",
|
||||
"Teal": "008080",
|
||||
"Thistle": "d8bfd8",
|
||||
"Tomato": "ff6347",
|
||||
"Turquoise": "40e0d0",
|
||||
"Violet": "ee82ee",
|
||||
"Wheat": "f5deb3",
|
||||
"White": "ffffff",
|
||||
"WhiteSmoke": "f5f5f5",
|
||||
"Yellow": "ffff00",
|
||||
"YellowGreen": "9acd32"
|
||||
}
|
61
test/test-utils/mocks/mock-console/infer-color.ts
Normal file
61
test/test-utils/mocks/mock-console/infer-color.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { hslToHex } from "#utils/common";
|
||||
import chalk, { type ChalkInstance, type ForegroundColorName, foregroundColorNames } from "chalk";
|
||||
import colorMap from "./color-map.json";
|
||||
|
||||
export function inferColorFormat(data: [string, ...unknown[]]): ChalkInstance {
|
||||
// Remove all CSS format strings and find the first one containing something vaguely resembling a color
|
||||
data[0] = data[0].replaceAll("%c", "");
|
||||
const args = data.slice(1).filter(t => typeof t === "string");
|
||||
const color = findColorPrefix(args);
|
||||
|
||||
// If the color is within Chalk's native roster, use it directly.
|
||||
if ((foregroundColorNames as string[]).includes(color)) {
|
||||
return chalk[color as ForegroundColorName];
|
||||
}
|
||||
|
||||
// Otherwise, coerce it to hex before feeding it in.
|
||||
return getColor(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first string with a "color:" CSS directive in an argument list.
|
||||
* @param args - The arguments containing the color directive
|
||||
* @returns The found color, or `"green"` if none were found
|
||||
*/
|
||||
function findColorPrefix(args: string[]): string {
|
||||
for (const arg of args) {
|
||||
const match = /color:\s*(.+?)(?:;|$)/g.exec(arg);
|
||||
if (match === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return match[1];
|
||||
}
|
||||
return "green";
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce an arbitrary CSS color string to a Chalk instance.
|
||||
* @param color - The color to coerce
|
||||
* @returns The Chalk color equivalent.
|
||||
*/
|
||||
function getColor(color: string): ChalkInstance {
|
||||
if (/^#([a-z0-9]{3,4}|[a-z0-9]{6}|[a-z0-9]{8})$/i.test(color)) {
|
||||
// already in hex
|
||||
return chalk.hex(color);
|
||||
}
|
||||
|
||||
const rgbMatch = /^rgba?\((\d{1,3})%?,\s*(\d{1,3})%?,?\s*(\d{1,3})%?,\s*/i.exec(color);
|
||||
if (rgbMatch) {
|
||||
const [red, green, blue] = rgbMatch;
|
||||
return chalk.rgb(+red, +green, +blue);
|
||||
}
|
||||
|
||||
const hslMatch = /^hslv?\((\d{1,3}),\s*(\d{1,3})%,\s*(\d{1,3})%\)$/i.exec(color);
|
||||
if (hslMatch) {
|
||||
const [hue, saturation, light] = hslMatch;
|
||||
return chalk.hex(hslToHex(+hue, +saturation / 100, +light / 100));
|
||||
}
|
||||
|
||||
return chalk.hex(colorMap[color] ?? "#00ff95ff");
|
||||
}
|
140
test/test-utils/mocks/mock-console/mock-console.ts
Normal file
140
test/test-utils/mocks/mock-console/mock-console.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { inferColorFormat } from "#test/test-utils/mocks/mock-console/infer-color";
|
||||
import { coerceArray } from "#utils/common";
|
||||
import { Console } from "node:console";
|
||||
import { stderr, stdout } from "node:process";
|
||||
import util from "node:util";
|
||||
import chalk, { type ChalkInstance } from "chalk";
|
||||
|
||||
// Tell chalk we support truecolor
|
||||
chalk.level = 3;
|
||||
|
||||
// TODO: Review this
|
||||
const blacklist = [
|
||||
"variant icon does not exist", // Repetitive warnings about icons not found
|
||||
'Texture "%s" not found', // Repetitive warnings about textures not found
|
||||
"type: 'Pokemon',", // Large Pokemon objects
|
||||
"gameVersion: ", // Large session-data and system-data objects
|
||||
"Phaser v", // Phaser version text
|
||||
"Seed:", // Stuff about wave seed (we should really stop logging this shit)
|
||||
"Wave Seed:", // Stuff about wave seed (we should really stop logging this shit)
|
||||
];
|
||||
const whitelist = ["Start Phase"];
|
||||
|
||||
/**
|
||||
* The {@linkcode MockConsole} is a wrapper around the global {@linkcode console} object.
|
||||
* It automatically colors text and such.
|
||||
*/
|
||||
export class MockConsole extends Console {
|
||||
/**
|
||||
* A list of warnings that are queued to be displayed after all tests in the same file are finished.
|
||||
*/
|
||||
private static queuedWarnings: unknown[][] = [];
|
||||
|
||||
/**
|
||||
* Queue a warning to be printed after all tests in the same file are finished.
|
||||
*/
|
||||
// TODO: Add some warnings
|
||||
public static queuePostTestWarning(...data: unknown[]): void {
|
||||
MockConsole.queuedWarnings.push(data);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(stdout, stderr, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print all post-test warnings that have been queued, and then clears the queue.
|
||||
*/
|
||||
public static printPostTestWarnings() {
|
||||
for (const data of MockConsole.queuedWarnings) {
|
||||
console.warn(...data);
|
||||
}
|
||||
MockConsole.queuedWarnings = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given set of data is in the blacklist to be barred from logging.
|
||||
* @param data - The data being logged
|
||||
* @returns Whether `data` is blacklisted from console logging
|
||||
*/
|
||||
private checkBlacklist(data: unknown[]): boolean {
|
||||
const dataStr = this.getStr(data);
|
||||
return !whitelist.some(b => dataStr.includes(b)) && blacklist.some(b => dataStr.includes(b));
|
||||
}
|
||||
|
||||
public trace(...data: unknown[]) {
|
||||
if (this.checkBlacklist(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Figure out how to add color to the full trace text
|
||||
super.trace(...this.format(chalk.hex("#b700ff"), data));
|
||||
}
|
||||
|
||||
public debug(...data: unknown[]) {
|
||||
if (this.checkBlacklist(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.debug(...this.format(chalk.hex("#874600ff"), data));
|
||||
}
|
||||
|
||||
public log(...data: unknown[]): void {
|
||||
if (this.checkBlacklist(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let formatter: ChalkInstance | undefined;
|
||||
|
||||
if (data.some(d => typeof d === "string" && d.includes("color:"))) {
|
||||
// Infer the color format from the arguments, then remove everything but the message.
|
||||
formatter = inferColorFormat(data as [string, ...unknown[]]);
|
||||
data.splice(1);
|
||||
} else if (data[0] === "[UI]") {
|
||||
// Cyan for UI debug messages
|
||||
formatter = chalk.hex("#009dffff");
|
||||
} else if (typeof data[0] === "string" && data[0].startsWith("=====")) {
|
||||
// Orange logging for "New Turn"/etc messages
|
||||
formatter = chalk.hex("#ffad00ff");
|
||||
}
|
||||
|
||||
super.log(...this.format(formatter, data));
|
||||
}
|
||||
|
||||
public warn(...data: unknown[]) {
|
||||
if (this.checkBlacklist(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.warn(...this.format(chalk.yellow, data));
|
||||
}
|
||||
|
||||
public error(...data: unknown[]) {
|
||||
if (this.checkBlacklist(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.error(...this.format(chalk.redBright, data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable string representation of `data`.
|
||||
*/
|
||||
private getStr(data: unknown) {
|
||||
return util.inspect(data, { sorted: true, breakLength: 120 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringify the given data in a manner fit for logging.
|
||||
* @param color - A Chalk instance or other transformation function used to transform the output,
|
||||
* or `undefined` to not transform it at all.
|
||||
* @param data - The data that the format should be applied to.
|
||||
* @returns A stringified copy of `data` with {@linkcode color} applied to each individual argument.
|
||||
* @todo Do we need to apply color to each entry or just run it through `util.format`?
|
||||
*/
|
||||
private format(color: ((s: unknown) => unknown) | undefined, data: unknown | unknown[]): unknown[] {
|
||||
data = coerceArray(data);
|
||||
color ??= a => a;
|
||||
return (data as unknown[]).map(a => color(typeof a === "function" || typeof a === "object" ? this.getStr(a) : a));
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import type { MockGameObject } from "#test/test-utils/mocks/mock-game-object";
|
||||
import type { TextInterceptor } from "#test/test-utils/text-interceptor";
|
||||
import { UI } from "#ui/ui";
|
||||
|
||||
export class MockText implements MockGameObject {
|
||||
@ -82,13 +83,14 @@ export class MockText implements MockGameObject {
|
||||
|
||||
showText(
|
||||
text: string,
|
||||
delay?: number | null,
|
||||
_delay?: number | null,
|
||||
callback?: Function | null,
|
||||
callbackDelay?: number | null,
|
||||
prompt?: boolean | null,
|
||||
promptDelay?: number | null,
|
||||
_callbackDelay?: number | null,
|
||||
_prompt?: boolean | null,
|
||||
_promptDelay?: number | null,
|
||||
) {
|
||||
this.scene.messageWrapper.showText(text, delay, callback, callbackDelay, prompt, promptDelay);
|
||||
// TODO: this is a very bad way to pass calls around
|
||||
(this.scene.messageWrapper as TextInterceptor).showText(text);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
@ -96,13 +98,13 @@ export class MockText implements MockGameObject {
|
||||
|
||||
showDialogue(
|
||||
keyOrText: string,
|
||||
name: string | undefined,
|
||||
delay: number | null = 0,
|
||||
name: string,
|
||||
_delay: number | null,
|
||||
callback: Function,
|
||||
callbackDelay?: number,
|
||||
promptDelay?: number,
|
||||
_callbackDelay?: number,
|
||||
_promptDelay?: number,
|
||||
) {
|
||||
this.scene.messageWrapper.showDialogue(keyOrText, name, delay, callback, callbackDelay, promptDelay);
|
||||
(this.scene.messageWrapper as TextInterceptor).showDialogue(keyOrText, name);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { SESSION_ID_COOKIE_NAME } from "#app/constants";
|
||||
import { initializeGame } from "#app/init/init";
|
||||
import { initI18n } from "#plugins/i18n";
|
||||
import { blobToString } from "#test/test-utils/game-manager-utils";
|
||||
import { manageListeners } from "#test/test-utils/listeners-manager";
|
||||
import { MockConsoleLog } from "#test/test-utils/mocks/mock-console-log";
|
||||
import { MockConsole } from "#test/test-utils/mocks/mock-console/mock-console";
|
||||
import { mockContext } from "#test/test-utils/mocks/mock-context-canvas";
|
||||
import { mockLocalStorage } from "#test/test-utils/mocks/mock-local-storage";
|
||||
import { MockImage } from "#test/test-utils/mocks/mocks-container/mock-image";
|
||||
@ -12,40 +10,35 @@ import Phaser from "phaser";
|
||||
import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
|
||||
import InputText from "phaser3-rex-plugins/plugins/inputtext";
|
||||
|
||||
let wasInitialized = false;
|
||||
|
||||
/**
|
||||
* Run initialization code upon starting a new file, both per-suite and per-instance oncess.
|
||||
* Run per-suite initialization code upon starting a new file.
|
||||
*/
|
||||
export function initTests(): void {
|
||||
setupStubs();
|
||||
if (!wasInitialized) {
|
||||
initTestFile();
|
||||
wasInitialized = true;
|
||||
}
|
||||
|
||||
manageListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize various values at the beginning of each testing instance.
|
||||
*/
|
||||
function initTestFile(): void {
|
||||
initI18n();
|
||||
initializeGame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup various stubs for testing.
|
||||
* @todo Move this into a dedicated stub file instead of running it once per test instance
|
||||
* @todo review these to see which are actually necessary
|
||||
* @todo Investigate why this resets on new test suite start
|
||||
*/
|
||||
function setupStubs(): void {
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
value: mockLocalStorage(),
|
||||
});
|
||||
Object.defineProperty(window, "console", {
|
||||
value: new MockConsoleLog(false),
|
||||
console.log(console instanceof MockConsole);
|
||||
console.log(Phaser.GameObjects.Image instanceof MockImage);
|
||||
Object.defineProperties(global, {
|
||||
localStorage: {
|
||||
value: mockLocalStorage(),
|
||||
},
|
||||
console: {
|
||||
value: new MockConsole(),
|
||||
},
|
||||
matchMedia: {
|
||||
value: () => ({
|
||||
matches: false,
|
||||
}),
|
||||
},
|
||||
});
|
||||
Object.defineProperty(document, "fonts", {
|
||||
writable: true,
|
||||
@ -69,11 +62,6 @@ function setupStubs(): void {
|
||||
navigator.getGamepads = () => [];
|
||||
setCookie(SESSION_ID_COOKIE_NAME, "fake_token");
|
||||
|
||||
window.matchMedia = () =>
|
||||
({
|
||||
matches: false,
|
||||
}) as any;
|
||||
|
||||
/**
|
||||
* Sets this object's position relative to another object with a given offset
|
||||
* @param guideObject - The {@linkcode Phaser.GameObjects.GameObject} to base the position off of
|
||||
|
@ -1,39 +1,49 @@
|
||||
import type { BattleScene } from "#app/battle-scene";
|
||||
import chalk from "chalk";
|
||||
|
||||
/**
|
||||
* Class will intercept any text or dialogue message calls and log them for test purposes
|
||||
* The {@linkcode TextInterceptor} is a wrapper class that intercepts and logs any messages
|
||||
* that would be displayed on-screen.
|
||||
*/
|
||||
export class TextInterceptor {
|
||||
private scene;
|
||||
public logs: string[] = [];
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
/** A log containing messages having been displayed on screen, sorted in FIFO order. */
|
||||
public readonly logs: string[] = [];
|
||||
|
||||
constructor(scene: BattleScene) {
|
||||
// @ts-expect-error: Find another more sanitary way of doing this
|
||||
scene.messageWrapper = this;
|
||||
}
|
||||
|
||||
showText(
|
||||
text: string,
|
||||
_delay?: number,
|
||||
_callback?: Function,
|
||||
_callbackDelay?: number,
|
||||
_prompt?: boolean,
|
||||
_promptDelay?: number,
|
||||
): void {
|
||||
console.log(text);
|
||||
/** Clear the current content of the TextInterceptor. */
|
||||
public clearLogs(): void {
|
||||
this.logs.splice(0);
|
||||
}
|
||||
|
||||
showText(text: string): void {
|
||||
// NB: We do not format the raw _logs_ themselves as tests will be actively checking it.
|
||||
console.log(this.formatText(text));
|
||||
this.logs.push(text);
|
||||
}
|
||||
|
||||
showDialogue(
|
||||
text: string,
|
||||
name: string,
|
||||
_delay?: number,
|
||||
_callback?: Function,
|
||||
_callbackDelay?: number,
|
||||
_promptDelay?: number,
|
||||
): void {
|
||||
console.log(name, text);
|
||||
showDialogue(text: string, name: string): void {
|
||||
console.log(`${name}: \n${this.formatText(text)}`);
|
||||
this.logs.push(name, text);
|
||||
}
|
||||
|
||||
getLatestMessage(): string {
|
||||
return this.logs.pop() ?? "";
|
||||
/**
|
||||
* Format text to be displayed to the test console, as follows:
|
||||
* 1. Replaces new lines and new text boxes (marked by `$`) with indented new lines.
|
||||
* 2. Removes all `@c{}`, `@d{}`, `@s{}`, and `@f{}` flags from the text.
|
||||
* 3. Makes text blue
|
||||
* @param text - The unformatted text
|
||||
* @returns The formatted text
|
||||
*/
|
||||
private formatText(text: string): string {
|
||||
return chalk.blue(
|
||||
text
|
||||
.replace(/\n/g, " ")
|
||||
.replace(/\$/g, "\n ")
|
||||
.replace(/@\w{.*?}/g, ""),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import "vitest-canvas-mock";
|
||||
import { initializeGame } from "#app/init/init";
|
||||
import { MockConsole } from "#test/test-utils/mocks/mock-console/mock-console";
|
||||
import { initTests } from "#test/test-utils/test-file-initialization";
|
||||
import chalk from "chalk";
|
||||
import { afterAll, beforeAll, vi } from "vitest";
|
||||
|
||||
/** Set the timezone to UTC for tests. */
|
||||
@ -14,6 +17,20 @@ vi.mock("#app/overrides", async importOriginal => {
|
||||
} satisfies typeof import("#app/overrides");
|
||||
});
|
||||
|
||||
//#region Mocking
|
||||
|
||||
/** Mock the override import to always return default values, ignoring any custom overrides. */
|
||||
vi.mock("#app/overrides", async importOriginal => {
|
||||
const { defaultOverrides } = await importOriginal<typeof import("#app/overrides")>();
|
||||
|
||||
return {
|
||||
default: defaultOverrides,
|
||||
// Export `defaultOverrides` as a *copy*.
|
||||
// This ensures we can easily reset `overrides` back to its default values after modifying it.
|
||||
defaultOverrides: { ...defaultOverrides },
|
||||
} satisfies typeof import("#app/overrides");
|
||||
});
|
||||
|
||||
/**
|
||||
* This is a hacky way to mock the i18n backend requests (with the help of {@link https://mswjs.io/ | msw}).
|
||||
* The reason to put it inside of a mock is to elevate it.
|
||||
@ -30,8 +47,11 @@ vi.mock("i18next", async importOriginal => {
|
||||
const filename = req.params[0];
|
||||
|
||||
try {
|
||||
const json = await import(`../public/locales/en/${req.params[0]}`);
|
||||
console.log("Loaded locale", filename);
|
||||
const localeFiles = import.meta.glob("../public/locales/en/**/*.json", { eager: true });
|
||||
const json = localeFiles[`../public/locales/en/${filename}`] || {};
|
||||
if (import.meta.env.VITE_I18N_DEBUG === "1") {
|
||||
console.log("Loaded locale", filename);
|
||||
}
|
||||
return HttpResponse.json(json);
|
||||
} catch (err) {
|
||||
console.log(`Failed to load locale ${filename}!`, err);
|
||||
@ -48,13 +68,25 @@ vi.mock("i18next", async importOriginal => {
|
||||
return await importOriginal();
|
||||
});
|
||||
|
||||
/** Ensure that i18n is initialized on all calls. */
|
||||
// TODO: Initialize i18n directly on import instead of initializing it during importing of trainer code
|
||||
vi.mock("#app/plugins/i18n", async importOriginal => {
|
||||
const importedStuff = await importOriginal<typeof import("#app/plugins/i18n")>();
|
||||
const { initI18n } = importedStuff;
|
||||
await initI18n();
|
||||
return importedStuff;
|
||||
});
|
||||
|
||||
global.testFailed = false;
|
||||
|
||||
initializeGame();
|
||||
|
||||
beforeAll(() => {
|
||||
initTests();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
global.server.close();
|
||||
console.log("Closing i18n MSW server!");
|
||||
MockConsole.printPostTestWarnings();
|
||||
console.log(chalk.hex("#dfb8d8")("Closing i18n MSW server!"));
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user