mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-25 16:59:27 +02:00
Compare commits
22 Commits
85d55bf9c6
...
0d0a4fad1f
Author | SHA1 | Date | |
---|---|---|---|
|
0d0a4fad1f | ||
|
076ef81691 | ||
|
23271901cf | ||
|
1517e0512e | ||
|
1d1ad8e9dc | ||
|
24f3d75055 | ||
|
839a099b3a | ||
|
c07f786042 | ||
|
6866248b41 | ||
|
9298ff8282 | ||
|
7e7ca6b3fa | ||
|
cd890025d1 | ||
|
71801fe298 | ||
|
e968063eaa | ||
|
b98ff5ae90 | ||
|
c89accc673 | ||
|
df8d1dc8c7 | ||
|
9455030fbe | ||
|
dbea701d6d | ||
|
641f5f5b97 | ||
|
f5154179b3 | ||
|
49825a6729 |
@ -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.
|
- 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.
|
[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.
|
- 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).
|
3. Your locales should use the following format:
|
||||||
4. The Translation Team will approve the locale PR (after corrections, if necessary), then merge it into `pokerogue-locales`.
|
- File names should be in `kebab-case`. Example: `trainer-names.json`
|
||||||
5. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment.
|
- 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).
|
[^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.
|
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.
|
||||||
|
@ -11,7 +11,7 @@ import { BooleanHolder, toDmgValue } from "#utils/common";
|
|||||||
* These are the moves assigned to a {@linkcode Pokemon} object.
|
* These are the moves assigned to a {@linkcode Pokemon} object.
|
||||||
* It links to {@linkcode Move} class via the move ID.
|
* It links to {@linkcode Move} class via the move ID.
|
||||||
* Compared to {@linkcode Move}, this class also tracks things like
|
* Compared to {@linkcode Move}, this class also tracks things like
|
||||||
* PP Ups recieved, PP used, etc.
|
* PP Ups received, PP used, etc.
|
||||||
* @see {@linkcode isUsable} - checks if move is restricted, out of PP, or not implemented.
|
* @see {@linkcode isUsable} - checks if move is restricted, out of PP, or not implemented.
|
||||||
* @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID.
|
* @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID.
|
||||||
* @see {@linkcode usePp} - removes a point of PP from the move.
|
* @see {@linkcode usePp} - removes a point of PP from the move.
|
||||||
|
@ -38,6 +38,7 @@ export enum UiMode {
|
|||||||
UNAVAILABLE,
|
UNAVAILABLE,
|
||||||
CHALLENGE_SELECT,
|
CHALLENGE_SELECT,
|
||||||
RENAME_POKEMON,
|
RENAME_POKEMON,
|
||||||
|
RENAME_RUN,
|
||||||
RUN_HISTORY,
|
RUN_HISTORY,
|
||||||
RUN_INFO,
|
RUN_INFO,
|
||||||
TEST_DIALOGUE,
|
TEST_DIALOGUE,
|
||||||
|
@ -127,6 +127,7 @@ export interface SessionSaveData {
|
|||||||
battleType: BattleType;
|
battleType: BattleType;
|
||||||
trainer: TrainerData;
|
trainer: TrainerData;
|
||||||
gameVersion: string;
|
gameVersion: string;
|
||||||
|
runNameText: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
challenges: ChallengeData[];
|
challenges: ChallengeData[];
|
||||||
mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME,
|
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> {
|
loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
|
||||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
|
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
|
||||||
return new Promise(async (resolve, reject) => {
|
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.setOrigin(0, 0);
|
||||||
headerText.setPositionRelative(headerBg, 8, 4);
|
headerText.setPositionRelative(headerBg, 8, 4);
|
||||||
this.runContainer.add(headerText);
|
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 { GameMode } from "#app/game-mode";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { Button } from "#enums/buttons";
|
import { Button } from "#enums/buttons";
|
||||||
|
import { GameModes } from "#enums/game-modes";
|
||||||
import { TextStyle } from "#enums/text-style";
|
import { TextStyle } from "#enums/text-style";
|
||||||
import { UiMode } from "#enums/ui-mode";
|
import { UiMode } from "#enums/ui-mode";
|
||||||
// biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts`
|
// biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts`
|
||||||
import * as Modifier from "#modifiers/modifier";
|
import * as Modifier from "#modifiers/modifier";
|
||||||
import type { SessionSaveData } from "#system/game-data";
|
import type { SessionSaveData } from "#system/game-data";
|
||||||
import type { PokemonData } from "#system/pokemon-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 { MessageUiHandler } from "#ui/message-ui-handler";
|
||||||
import { RunDisplayMode } from "#ui/run-info-ui-handler";
|
import { RunDisplayMode } from "#ui/run-info-ui-handler";
|
||||||
import { addTextObject } from "#ui/text";
|
import { addTextObject } from "#ui/text";
|
||||||
@ -15,7 +17,7 @@ import { fixedInt, formatLargeNumber, getPlayTimeString, isNullOrUndefined } fro
|
|||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
const SESSION_SLOTS_COUNT = 5;
|
const SESSION_SLOTS_COUNT = 5;
|
||||||
const SLOTS_ON_SCREEN = 3;
|
const SLOTS_ON_SCREEN = 2;
|
||||||
|
|
||||||
export enum SaveSlotUiMode {
|
export enum SaveSlotUiMode {
|
||||||
LOAD,
|
LOAD,
|
||||||
@ -33,6 +35,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
|
|
||||||
private uiMode: SaveSlotUiMode;
|
private uiMode: SaveSlotUiMode;
|
||||||
private saveSlotSelectCallback: SaveSlotSelectCallback | null;
|
private saveSlotSelectCallback: SaveSlotSelectCallback | null;
|
||||||
|
protected manageDataConfig: OptionSelectConfig;
|
||||||
|
|
||||||
private scrollCursor = 0;
|
private scrollCursor = 0;
|
||||||
|
|
||||||
@ -101,6 +104,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
|
|
||||||
processInput(button: Button): boolean {
|
processInput(button: Button): boolean {
|
||||||
const ui = this.getUi();
|
const ui = this.getUi();
|
||||||
|
const manageDataOptions: any[] = [];
|
||||||
|
|
||||||
let success = false;
|
let success = false;
|
||||||
let error = false;
|
let error = false;
|
||||||
@ -109,14 +113,115 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
const originalCallback = this.saveSlotSelectCallback;
|
const originalCallback = this.saveSlotSelectCallback;
|
||||||
if (button === Button.ACTION) {
|
if (button === Button.ACTION) {
|
||||||
const cursor = this.cursor + this.scrollCursor;
|
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;
|
error = true;
|
||||||
} else {
|
} else {
|
||||||
switch (this.uiMode) {
|
switch (this.uiMode) {
|
||||||
case SaveSlotUiMode.LOAD:
|
case SaveSlotUiMode.LOAD:
|
||||||
this.saveSlotSelectCallback = null;
|
if (!sessionSlot.malformed) {
|
||||||
|
manageDataOptions.push({
|
||||||
|
label: i18next.t("menu:loadGame"),
|
||||||
|
handler: () => {
|
||||||
|
globalScene.ui.revertMode();
|
||||||
originalCallback?.(cursor);
|
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;
|
break;
|
||||||
|
|
||||||
case SaveSlotUiMode.SAVE: {
|
case SaveSlotUiMode.SAVE: {
|
||||||
const saveAndCallback = () => {
|
const saveAndCallback = () => {
|
||||||
const originalCallback = this.saveSlotSelectCallback;
|
const originalCallback = this.saveSlotSelectCallback;
|
||||||
@ -161,6 +266,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.saveSlotSelectCallback = null;
|
this.saveSlotSelectCallback = null;
|
||||||
|
ui.showText("", 0);
|
||||||
originalCallback?.(-1);
|
originalCallback?.(-1);
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
@ -267,33 +373,34 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
this.cursorObj = globalScene.add.container(0, 0);
|
this.cursorObj = globalScene.add.container(0, 0);
|
||||||
const cursorBox = globalScene.add.nineslice(
|
const cursorBox = globalScene.add.nineslice(
|
||||||
0,
|
0,
|
||||||
0,
|
15,
|
||||||
"select_cursor_highlight_thick",
|
"select_cursor_highlight_thick",
|
||||||
undefined,
|
undefined,
|
||||||
296,
|
294,
|
||||||
44,
|
this.sessionSlots[prevSlotIndex ?? 0]?.saveData?.runNameText ? 50 : 60,
|
||||||
6,
|
6,
|
||||||
6,
|
6,
|
||||||
6,
|
6,
|
||||||
6,
|
6,
|
||||||
);
|
);
|
||||||
const rightArrow = globalScene.add.image(0, 0, "cursor");
|
const rightArrow = globalScene.add.image(0, 0, "cursor");
|
||||||
rightArrow.setPosition(160, 0);
|
rightArrow.setPosition(160, 15);
|
||||||
rightArrow.setName("rightArrow");
|
rightArrow.setName("rightArrow");
|
||||||
this.cursorObj.add([cursorBox, rightArrow]);
|
this.cursorObj.add([cursorBox, rightArrow]);
|
||||||
this.sessionSlotsContainer.add(this.cursorObj);
|
this.sessionSlotsContainer.add(this.cursorObj);
|
||||||
}
|
}
|
||||||
const cursorPosition = cursor + this.scrollCursor;
|
const cursorPosition = cursor + this.scrollCursor;
|
||||||
const cursorIncrement = cursorPosition * 56;
|
const cursorIncrement = cursorPosition * 76;
|
||||||
if (this.sessionSlots[cursorPosition] && this.cursorObj) {
|
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.
|
// 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.
|
// Only session slots with session data will move leftwards and have a visible arrow.
|
||||||
if (!hasData) {
|
if (!hasData) {
|
||||||
this.cursorObj.setPosition(151, 26 + cursorIncrement);
|
this.cursorObj.setPosition(151, 20 + cursorIncrement);
|
||||||
this.sessionSlots[cursorPosition].setPosition(0, cursorIncrement);
|
this.sessionSlots[cursorPosition].setPosition(0, cursorIncrement);
|
||||||
} else {
|
} else {
|
||||||
this.cursorObj.setPosition(145, 26 + cursorIncrement);
|
this.cursorObj.setPosition(145, 20 + cursorIncrement);
|
||||||
this.sessionSlots[cursorPosition].setPosition(-6, cursorIncrement);
|
this.sessionSlots[cursorPosition].setPosition(-6, cursorIncrement);
|
||||||
}
|
}
|
||||||
this.setArrowVisibility(hasData);
|
this.setArrowVisibility(hasData);
|
||||||
@ -311,7 +418,8 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
revertSessionSlot(slotIndex: number): void {
|
revertSessionSlot(slotIndex: number): void {
|
||||||
const sessionSlot = this.sessionSlots[slotIndex];
|
const sessionSlot = this.sessionSlots[slotIndex];
|
||||||
if (sessionSlot) {
|
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);
|
this.setCursor(this.cursor, prevSlotIndex);
|
||||||
globalScene.tweens.add({
|
globalScene.tweens.add({
|
||||||
targets: this.sessionSlotsContainer,
|
targets: this.sessionSlotsContainer,
|
||||||
y: this.sessionSlotsContainerInitialY - 56 * scrollCursor,
|
y: this.sessionSlotsContainerInitialY - 76 * scrollCursor,
|
||||||
duration: fixedInt(325),
|
duration: fixedInt(325),
|
||||||
ease: "Sine.easeInOut",
|
ease: "Sine.easeInOut",
|
||||||
});
|
});
|
||||||
@ -374,12 +482,14 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
class SessionSlot extends Phaser.GameObjects.Container {
|
class SessionSlot extends Phaser.GameObjects.Container {
|
||||||
public slotId: number;
|
public slotId: number;
|
||||||
public hasData: boolean;
|
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;
|
private loadingLabel: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
public saveData: SessionSaveData;
|
public saveData: SessionSaveData;
|
||||||
|
|
||||||
constructor(slotId: number) {
|
constructor(slotId: number) {
|
||||||
super(globalScene, 0, slotId * 56);
|
super(globalScene, 0, slotId * 76);
|
||||||
|
|
||||||
this.slotId = slotId;
|
this.slotId = slotId;
|
||||||
|
|
||||||
@ -387,32 +497,89 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const slotWindow = addWindow(0, 0, 304, 52);
|
this.slotWindow = addWindow(0, 0, 304, 70);
|
||||||
this.add(slotWindow);
|
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.loadingLabel.setOrigin(0.5, 0.5);
|
||||||
this.add(this.loadingLabel);
|
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) {
|
async setupWithData(data: SessionSaveData) {
|
||||||
|
const hasName = data?.runNameText;
|
||||||
this.remove(this.loadingLabel, true);
|
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(
|
const gameModeLabel = addTextObject(
|
||||||
8,
|
8,
|
||||||
5,
|
19,
|
||||||
`${GameMode.getModeName(data.gameMode) || i18next.t("gameMode:unknown")} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${data.waveIndex}`,
|
`${GameMode.getModeName(data.gameMode) || i18next.t("gameMode:unknown")} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${data.waveIndex}`,
|
||||||
TextStyle.WINDOW,
|
TextStyle.WINDOW,
|
||||||
);
|
);
|
||||||
this.add(gameModeLabel);
|
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);
|
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);
|
this.add(playTimeLabel);
|
||||||
|
|
||||||
const pokemonIconsContainer = globalScene.add.container(144, 4);
|
const pokemonIconsContainer = globalScene.add.container(144, 16);
|
||||||
data.party.forEach((p: PokemonData, i: number) => {
|
data.party.forEach((p: PokemonData, i: number) => {
|
||||||
const iconContainer = globalScene.add.container(26 * i, 0);
|
const iconContainer = globalScene.add.container(26 * i, 0);
|
||||||
iconContainer.setScale(0.75);
|
iconContainer.setScale(0.75);
|
||||||
@ -427,13 +594,9 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
|||||||
TextStyle.PARTY,
|
TextStyle.PARTY,
|
||||||
{ fontSize: "54px", color: "#f8f8f8" },
|
{ fontSize: "54px", color: "#f8f8f8" },
|
||||||
);
|
);
|
||||||
text.setShadow(0, 0, undefined);
|
text.setShadow(0, 0, undefined).setStroke("#424242", 14).setOrigin(1, 0);
|
||||||
text.setStroke("#424242", 14);
|
|
||||||
text.setOrigin(1, 0);
|
|
||||||
|
|
||||||
iconContainer.add(icon);
|
|
||||||
iconContainer.add(text);
|
|
||||||
|
|
||||||
|
iconContainer.add([icon, text]);
|
||||||
pokemonIconsContainer.add(iconContainer);
|
pokemonIconsContainer.add(iconContainer);
|
||||||
|
|
||||||
pokemon.destroy();
|
pokemon.destroy();
|
||||||
@ -441,7 +604,7 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
this.add(pokemonIconsContainer);
|
this.add(pokemonIconsContainer);
|
||||||
|
|
||||||
const modifierIconsContainer = globalScene.add.container(148, 30);
|
const modifierIconsContainer = globalScene.add.container(148, 38);
|
||||||
modifierIconsContainer.setScale(0.5);
|
modifierIconsContainer.setScale(0.5);
|
||||||
let visibleModifierIndex = 0;
|
let visibleModifierIndex = 0;
|
||||||
for (const m of data.modifiers) {
|
for (const m of data.modifiers) {
|
||||||
@ -464,20 +627,31 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
load(): Promise<boolean> {
|
load(): Promise<boolean> {
|
||||||
return new Promise<boolean>(resolve => {
|
return new Promise<boolean>(resolve => {
|
||||||
globalScene.gameData.getSession(this.slotId).then(async sessionData => {
|
globalScene.gameData
|
||||||
|
.getSession(this.slotId)
|
||||||
|
.then(async sessionData => {
|
||||||
// Ignore the results if the view was exited
|
// Ignore the results if the view was exited
|
||||||
if (!this.active) {
|
if (!this.active) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.hasData = !!sessionData;
|
||||||
if (!sessionData) {
|
if (!sessionData) {
|
||||||
this.hasData = false;
|
|
||||||
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));
|
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.hasData = true;
|
|
||||||
this.saveData = sessionData;
|
this.saveData = sessionData;
|
||||||
await this.setupWithData(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);
|
resolve(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -60,6 +60,7 @@ import { addWindow } from "#ui/ui-theme";
|
|||||||
import { UnavailableModalUiHandler } from "#ui/unavailable-modal-ui-handler";
|
import { UnavailableModalUiHandler } from "#ui/unavailable-modal-ui-handler";
|
||||||
import { executeIf } from "#utils/common";
|
import { executeIf } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
import { RenameRunFormUiHandler } from "./rename-run-ui-handler";
|
||||||
|
|
||||||
const transitionModes = [
|
const transitionModes = [
|
||||||
UiMode.SAVE_SLOT,
|
UiMode.SAVE_SLOT,
|
||||||
@ -98,6 +99,7 @@ const noTransitionModes = [
|
|||||||
UiMode.SESSION_RELOAD,
|
UiMode.SESSION_RELOAD,
|
||||||
UiMode.UNAVAILABLE,
|
UiMode.UNAVAILABLE,
|
||||||
UiMode.RENAME_POKEMON,
|
UiMode.RENAME_POKEMON,
|
||||||
|
UiMode.RENAME_RUN,
|
||||||
UiMode.TEST_DIALOGUE,
|
UiMode.TEST_DIALOGUE,
|
||||||
UiMode.AUTO_COMPLETE,
|
UiMode.AUTO_COMPLETE,
|
||||||
UiMode.ADMIN,
|
UiMode.ADMIN,
|
||||||
@ -168,6 +170,7 @@ export class UI extends Phaser.GameObjects.Container {
|
|||||||
new UnavailableModalUiHandler(),
|
new UnavailableModalUiHandler(),
|
||||||
new GameChallengesUiHandler(),
|
new GameChallengesUiHandler(),
|
||||||
new RenameFormUiHandler(),
|
new RenameFormUiHandler(),
|
||||||
|
new RenameRunFormUiHandler(),
|
||||||
new RunHistoryUiHandler(),
|
new RunHistoryUiHandler(),
|
||||||
new RunInfoUiHandler(),
|
new RunInfoUiHandler(),
|
||||||
new TestDialogueUiHandler(UiMode.TEST_DIALOGUE),
|
new TestDialogueUiHandler(UiMode.TEST_DIALOGUE),
|
||||||
|
27
test/@types/test-helpers.ts
Normal file
27
test/@types/test-helpers.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import type { AtLeastOne, NonFunctionPropertiesRecursive as nonFunc } from "#types/type-helpers";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper type to admit an object containing the given properties
|
||||||
|
* _and_ at least 1 other non-function property.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* type foo = {
|
||||||
|
* qux: 1 | 2 | 3,
|
||||||
|
* bar: number,
|
||||||
|
* baz: string
|
||||||
|
* quux: () => void; // ignored!
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* type quxAndSomethingElse = OneOther<foo, "qux">
|
||||||
|
*
|
||||||
|
* const good1: quxAndSomethingElse = {qux: 1, bar: 3} // OK!
|
||||||
|
* const good2: quxAndSomethingElse = {qux: 2, baz: "4", bar: 12} // OK!
|
||||||
|
* const bad1: quxAndSomethingElse = {baz: "4", bar: 12} // Errors because `qux` is required
|
||||||
|
* const bad2: quxAndSomethingElse = {qux: 1} // Errors because at least 1 thing _other_ than `qux` is required
|
||||||
|
* ```
|
||||||
|
* @typeParam O - The object to source keys from
|
||||||
|
* @typeParam K - One or more of O's keys to render mandatory
|
||||||
|
*/
|
||||||
|
export type OneOther<O extends object, K extends keyof O> = AtLeastOne<Omit<nonFunc<O>, K>> & {
|
||||||
|
[key in K]: O[K];
|
||||||
|
};
|
136
test/@types/vitest.d.ts
vendored
136
test/@types/vitest.d.ts
vendored
@ -1,23 +1,32 @@
|
|||||||
import type { TerrainType } from "#app/data/terrain";
|
import type { TerrainType } from "#app/data/terrain";
|
||||||
|
import type { ArenaTag } from "#data/arena-tag";
|
||||||
import type { AbilityId } from "#enums/ability-id";
|
import type { AbilityId } from "#enums/ability-id";
|
||||||
|
import type { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
import type { BattlerTagType } from "#enums/battler-tag-type";
|
import type { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import type { MoveId } from "#enums/move-id";
|
import type { MoveId } from "#enums/move-id";
|
||||||
import type { PokemonType } from "#enums/pokemon-type";
|
import type { PokemonType } from "#enums/pokemon-type";
|
||||||
import type { BattleStat, EffectiveStat, Stat } from "#enums/stat";
|
import type { BattleStat, EffectiveStat, Stat } from "#enums/stat";
|
||||||
import type { StatusEffect } from "#enums/status-effect";
|
import type { StatusEffect } from "#enums/status-effect";
|
||||||
import type { WeatherType } from "#enums/weather-type";
|
import type { WeatherType } from "#enums/weather-type";
|
||||||
|
import type { Arena } from "#field/arena";
|
||||||
import type { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import type { ToHaveEffectiveStatMatcherOptions } from "#test/test-utils/matchers/to-have-effective-stat";
|
import type { toHaveEffectiveStatOptions } from "#test/test-utils/matchers/to-have-effective-stat";
|
||||||
import type { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect";
|
import type { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect";
|
||||||
import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types";
|
import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types";
|
||||||
import type { TurnMove } from "#types/turn-move";
|
import type { TurnMove } from "#types/turn-move";
|
||||||
import type { AtLeastOne } from "#types/type-helpers";
|
import type { AtLeastOne } from "#types/type-helpers";
|
||||||
|
import type { toDmgValue } from "utils/common";
|
||||||
import type { expect } from "vitest";
|
import type { expect } from "vitest";
|
||||||
|
import type { PositionalTag } from "#data/positional-tags/positional-tag";
|
||||||
|
import { PositionalTagType } from "#enums/positional-tag-type";
|
||||||
import type Overrides from "#app/overrides";
|
import type Overrides from "#app/overrides";
|
||||||
|
import type { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
import type { PokemonMove } from "#moves/pokemon-move";
|
import type { PokemonMove } from "#moves/pokemon-move";
|
||||||
|
import { toHaveArenaTagOptions } from "#test/test-utils/matchers/to-have-arena-tag";
|
||||||
|
import { toHavePositionalTagOptions } from "#test/test-utils/matchers/to-have-positional-tag";
|
||||||
|
|
||||||
declare module "vitest" {
|
declare module "vitest" {
|
||||||
interface Assertion {
|
interface Assertion<T> {
|
||||||
/**
|
/**
|
||||||
* Check whether an array contains EXACTLY the given items (in any order).
|
* Check whether an array contains EXACTLY the given items (in any order).
|
||||||
*
|
*
|
||||||
@ -27,45 +36,9 @@ declare module "vitest" {
|
|||||||
* @param expected - The expected contents of the array, in any order
|
* @param expected - The expected contents of the array, in any order
|
||||||
* @see {@linkcode expect.arrayContaining}
|
* @see {@linkcode expect.arrayContaining}
|
||||||
*/
|
*/
|
||||||
toEqualArrayUnsorted<E>(expected: E[]): void;
|
toEqualArrayUnsorted(expected: T[]): void;
|
||||||
|
|
||||||
/**
|
// #region Arena Matchers
|
||||||
* Check whether a {@linkcode Pokemon}'s current typing includes the given types.
|
|
||||||
*
|
|
||||||
* @param expected - The expected types (in any order)
|
|
||||||
* @param options - The options passed to the matcher
|
|
||||||
*/
|
|
||||||
toHaveTypes(expected: [PokemonType, ...PokemonType[]], options?: toHaveTypesOptions): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Matcher to check the contents of a {@linkcode Pokemon}'s move history.
|
|
||||||
*
|
|
||||||
* @param expectedValue - The expected value; can be a {@linkcode MoveId} or a partially filled {@linkcode TurnMove}
|
|
||||||
* containing the desired properties to check
|
|
||||||
* @param index - The index of the move history entry to check, in order from most recent to least recent.
|
|
||||||
* Default `0` (last used move)
|
|
||||||
* @see {@linkcode Pokemon.getLastXMoves}
|
|
||||||
*/
|
|
||||||
toHaveUsedMove(expected: MoveId | AtLeastOne<TurnMove>, index?: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether a {@linkcode Pokemon}'s effective stat is as expected
|
|
||||||
* (checked after all stat value modifications).
|
|
||||||
*
|
|
||||||
* @param stat - The {@linkcode EffectiveStat} to check
|
|
||||||
* @param expectedValue - The expected value of {@linkcode stat}
|
|
||||||
* @param options - (Optional) The {@linkcode ToHaveEffectiveStatMatcherOptions}
|
|
||||||
* @remarks
|
|
||||||
* If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead.
|
|
||||||
*/
|
|
||||||
toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: ToHaveEffectiveStatMatcherOptions): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether a {@linkcode Pokemon} has taken a specific amount of damage.
|
|
||||||
* @param expectedDamageTaken - The expected amount of damage taken
|
|
||||||
* @param roundDown - Whether to round down {@linkcode expectedDamageTaken} with {@linkcode toDmgValue}; default `true`
|
|
||||||
*/
|
|
||||||
toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the current {@linkcode WeatherType} is as expected.
|
* Check whether the current {@linkcode WeatherType} is as expected.
|
||||||
@ -80,9 +53,60 @@ declare module "vitest" {
|
|||||||
toHaveTerrain(expectedTerrainType: TerrainType): void;
|
toHaveTerrain(expectedTerrainType: TerrainType): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether a {@linkcode Pokemon} is at full HP.
|
* Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}.
|
||||||
|
* @param expectedTag - A partially-filled {@linkcode ArenaTag} containing the desired properties
|
||||||
*/
|
*/
|
||||||
toHaveFullHp(): void;
|
toHaveArenaTag<A extends ArenaTagType>(expectedTag: toHaveArenaTagOptions<A>): void;
|
||||||
|
/**
|
||||||
|
* Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}.
|
||||||
|
* @param expectedType - The {@linkcode ArenaTagType} of the desired tag
|
||||||
|
* @param side - The {@linkcode ArenaTagSide | side(s) of the field} the tag should affect; default {@linkcode ArenaTagSide.BOTH}
|
||||||
|
*/
|
||||||
|
toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the current {@linkcode Arena} contains the given {@linkcode PositionalTag}.
|
||||||
|
* @param expectedTag - A partially-filled `PositionalTag` containing the desired properties
|
||||||
|
*/
|
||||||
|
toHavePositionalTag<P extends PositionalTagType>(expectedTag: toHavePositionalTagOptions<P>): void;
|
||||||
|
/**
|
||||||
|
* Check whether the current {@linkcode Arena} contains the given number of {@linkcode PositionalTag}s.
|
||||||
|
* @param expectedType - The {@linkcode PositionalTagType} of the desired tag
|
||||||
|
* @param count - The number of instances of {@linkcode expectedType} that should be active;
|
||||||
|
* defaults to `1` and must be within the range `[0, 4]`
|
||||||
|
*/
|
||||||
|
toHavePositionalTag(expectedType: PositionalTagType, count?: number): void;
|
||||||
|
|
||||||
|
// #endregion Arena Matchers
|
||||||
|
|
||||||
|
// #region Pokemon Matchers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon}'s current typing includes the given types.
|
||||||
|
* @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0`
|
||||||
|
* @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher
|
||||||
|
*/
|
||||||
|
toHaveTypes(expectedTypes: PokemonType[], options?: toHaveTypesOptions): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon} has used a move matching the given criteria.
|
||||||
|
* @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used,
|
||||||
|
* or a partially filled {@linkcode TurnMove} containing the desired properties to check
|
||||||
|
* @param index - The index of the move history entry to check, in order from most recent to least recent; default `0`
|
||||||
|
* @see {@linkcode Pokemon.getLastXMoves}
|
||||||
|
*/
|
||||||
|
toHaveUsedMove(expectedMove: MoveId | AtLeastOne<TurnMove>, index?: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon}'s effective stat is as expected
|
||||||
|
* (checked after all stat value modifications).
|
||||||
|
* @param stat - The {@linkcode EffectiveStat} to check
|
||||||
|
* @param expectedValue - The expected value of {@linkcode stat}
|
||||||
|
* @param options - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher
|
||||||
|
* @remarks
|
||||||
|
* If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead.
|
||||||
|
*/
|
||||||
|
toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}.
|
* Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}.
|
||||||
@ -106,7 +130,7 @@ declare module "vitest" {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}.
|
* Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}.
|
||||||
* @param expectedAbilityId - The expected {@linkcode AbilityId}
|
* @param expectedAbilityId - The `AbilityId` to check for
|
||||||
*/
|
*/
|
||||||
toHaveAbilityApplied(expectedAbilityId: AbilityId): void;
|
toHaveAbilityApplied(expectedAbilityId: AbilityId): void;
|
||||||
|
|
||||||
@ -116,24 +140,36 @@ declare module "vitest" {
|
|||||||
*/
|
*/
|
||||||
toHaveHp(expectedHp: number): void;
|
toHaveHp(expectedHp: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon} has taken a specific amount of damage.
|
||||||
|
* @param expectedDamageTaken - The expected amount of damage taken
|
||||||
|
* @param roundDown - Whether to round down `expectedDamageTaken` with {@linkcode toDmgValue}; default `true`
|
||||||
|
*/
|
||||||
|
toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}).
|
* Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}).
|
||||||
* @remarks
|
* @remarks
|
||||||
* When checking whether an enemy wild Pokemon is fainted, one must reference it in a variable _before_ the fainting effect occurs
|
* When checking whether an enemy wild Pokemon is fainted, one must store a reference to it in a variable _before_ the fainting effect occurs.
|
||||||
* as otherwise the Pokemon will be GC'ed and rendered `undefined`.
|
* Otherwise, the Pokemon will be removed from the field and garbage collected.
|
||||||
*/
|
*/
|
||||||
toHaveFainted(): void;
|
toHaveFainted(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon} is at full HP.
|
||||||
|
*/
|
||||||
|
toHaveFullHp(): void;
|
||||||
/**
|
/**
|
||||||
* Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves.
|
* Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves.
|
||||||
* @param expectedValue - The {@linkcode MoveId} of the {@linkcode PokemonMove} that should have consumed PP
|
* @param moveId - The `MoveId` corresponding to the {@linkcode PokemonMove} that should have consumed PP
|
||||||
* @param ppUsed - The numerical amount of PP that should have been consumed,
|
* @param ppUsed - The numerical amount of PP that should have been consumed,
|
||||||
* or `all` to indicate the move should be _out_ of PP
|
* or `all` to indicate the move should be _out_ of PP
|
||||||
* @remarks
|
* @remarks
|
||||||
* If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.OPP_MOVESET_OVERRIDE},
|
* If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.OPP_MOVESET_OVERRIDE}
|
||||||
* does not contain {@linkcode expectedMove}
|
* or does not contain exactly one copy of {@linkcode moveId}, this will fail the test.
|
||||||
* or contains the desired move more than once, this will fail the test.
|
|
||||||
*/
|
*/
|
||||||
toHaveUsedPP(expectedMove: MoveId, ppUsed: number | "all"): void;
|
toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void;
|
||||||
|
|
||||||
|
// #endregion Pokemon Matchers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted";
|
import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted";
|
||||||
import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied";
|
import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied";
|
||||||
|
import { toHaveArenaTag } from "#test/test-utils/matchers/to-have-arena-tag";
|
||||||
import { toHaveBattlerTag } from "#test/test-utils/matchers/to-have-battler-tag";
|
import { toHaveBattlerTag } from "#test/test-utils/matchers/to-have-battler-tag";
|
||||||
import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective-stat";
|
import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective-stat";
|
||||||
import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted";
|
import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted";
|
||||||
import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp";
|
import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp";
|
||||||
import { toHaveHp } from "#test/test-utils/matchers/to-have-hp";
|
import { toHaveHp } from "#test/test-utils/matchers/to-have-hp";
|
||||||
|
import { toHavePositionalTag } from "#test/test-utils/matchers/to-have-positional-tag";
|
||||||
import { toHaveStatStage } from "#test/test-utils/matchers/to-have-stat-stage";
|
import { toHaveStatStage } from "#test/test-utils/matchers/to-have-stat-stage";
|
||||||
import { toHaveStatusEffect } from "#test/test-utils/matchers/to-have-status-effect";
|
import { toHaveStatusEffect } from "#test/test-utils/matchers/to-have-status-effect";
|
||||||
import { toHaveTakenDamage } from "#test/test-utils/matchers/to-have-taken-damage";
|
import { toHaveTakenDamage } from "#test/test-utils/matchers/to-have-taken-damage";
|
||||||
@ -22,18 +24,20 @@ import { expect } from "vitest";
|
|||||||
|
|
||||||
expect.extend({
|
expect.extend({
|
||||||
toEqualArrayUnsorted,
|
toEqualArrayUnsorted,
|
||||||
|
toHaveWeather,
|
||||||
|
toHaveTerrain,
|
||||||
|
toHaveArenaTag,
|
||||||
|
toHavePositionalTag,
|
||||||
toHaveTypes,
|
toHaveTypes,
|
||||||
toHaveUsedMove,
|
toHaveUsedMove,
|
||||||
toHaveEffectiveStat,
|
toHaveEffectiveStat,
|
||||||
toHaveTakenDamage,
|
|
||||||
toHaveWeather,
|
|
||||||
toHaveTerrain,
|
|
||||||
toHaveFullHp,
|
|
||||||
toHaveStatusEffect,
|
toHaveStatusEffect,
|
||||||
toHaveStatStage,
|
toHaveStatStage,
|
||||||
toHaveBattlerTag,
|
toHaveBattlerTag,
|
||||||
toHaveAbilityApplied,
|
toHaveAbilityApplied,
|
||||||
toHaveHp,
|
toHaveHp,
|
||||||
|
toHaveTakenDamage,
|
||||||
|
toHaveFullHp,
|
||||||
toHaveFainted,
|
toHaveFainted,
|
||||||
toHaveUsedPP,
|
toHaveUsedPP,
|
||||||
});
|
});
|
||||||
|
@ -39,15 +39,6 @@ describe("Move - Wish", () => {
|
|||||||
.enemyLevel(100);
|
.enemyLevel(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Expect that wish is active with the specified number of attacks.
|
|
||||||
* @param numAttacks - The number of wish instances that should be queued; default `1`
|
|
||||||
*/
|
|
||||||
function expectWishActive(numAttacks = 1) {
|
|
||||||
const wishes = game.scene.arena.positionalTagManager["tags"].filter(t => t.tagType === PositionalTagType.WISH);
|
|
||||||
expect(wishes).toHaveLength(numAttacks);
|
|
||||||
}
|
|
||||||
|
|
||||||
it("should heal the Pokemon in the current slot for 50% of the user's maximum HP", async () => {
|
it("should heal the Pokemon in the current slot for 50% of the user's maximum HP", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.ALOMOMOLA, SpeciesId.BLISSEY]);
|
await game.classicMode.startBattle([SpeciesId.ALOMOMOLA, SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
@ -58,19 +49,19 @@ describe("Move - Wish", () => {
|
|||||||
game.move.use(MoveId.WISH);
|
game.move.use(MoveId.WISH);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expectWishActive();
|
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||||
|
|
||||||
game.doSwitchPokemon(1);
|
game.doSwitchPokemon(1);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expectWishActive(0);
|
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
|
||||||
expect(game.textInterceptor.logs).toContain(
|
expect(game.textInterceptor.logs).toContain(
|
||||||
i18next.t("arenaTag:wishTagOnAdd", {
|
i18next.t("arenaTag:wishTagOnAdd", {
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(alomomola),
|
pokemonNameWithAffix: getPokemonNameWithAffix(alomomola),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(alomomola.hp).toBe(1);
|
expect(alomomola).toHaveHp(1);
|
||||||
expect(blissey.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1);
|
expect(blissey).toHaveHp(toDmgValue(alomomola.getMaxHp() / 2) + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work if the user has full HP, but not if it already has an active Wish", async () => {
|
it("should work if the user has full HP, but not if it already has an active Wish", async () => {
|
||||||
@ -82,13 +73,13 @@ describe("Move - Wish", () => {
|
|||||||
game.move.use(MoveId.WISH);
|
game.move.use(MoveId.WISH);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expectWishActive();
|
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||||
|
|
||||||
game.move.use(MoveId.WISH);
|
game.move.use(MoveId.WISH);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expect(alomomola.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1);
|
expect(alomomola.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1);
|
||||||
expect(alomomola.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
expect(alomomola).toHaveUsedMove({ result: MoveResult.FAIL });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should function independently of Future Sight", async () => {
|
it("should function independently of Future Sight", async () => {
|
||||||
@ -103,7 +94,8 @@ describe("Move - Wish", () => {
|
|||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expectWishActive(1);
|
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||||
|
expect(game).toHavePositionalTag(PositionalTagType.DELAYED_ATTACK);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work in double battles and trigger in order of creation", async () => {
|
it("should work in double battles and trigger in order of creation", async () => {
|
||||||
@ -127,7 +119,7 @@ describe("Move - Wish", () => {
|
|||||||
await game.setTurnOrder(oldOrder.map(p => p.getBattlerIndex()));
|
await game.setTurnOrder(oldOrder.map(p => p.getBattlerIndex()));
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expectWishActive(4);
|
expect(game).toHavePositionalTag(PositionalTagType.WISH, 4);
|
||||||
|
|
||||||
// Lower speed to change turn order
|
// Lower speed to change turn order
|
||||||
alomomola.setStatStage(Stat.SPD, 6);
|
alomomola.setStatStage(Stat.SPD, 6);
|
||||||
@ -141,7 +133,7 @@ describe("Move - Wish", () => {
|
|||||||
await game.phaseInterceptor.to("PositionalTagPhase");
|
await game.phaseInterceptor.to("PositionalTagPhase");
|
||||||
|
|
||||||
// all wishes have activated and added healing phases
|
// all wishes have activated and added healing phases
|
||||||
expectWishActive(0);
|
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
|
||||||
|
|
||||||
const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase"));
|
const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase"));
|
||||||
expect(healPhases).toHaveLength(4);
|
expect(healPhases).toHaveLength(4);
|
||||||
@ -165,14 +157,14 @@ describe("Move - Wish", () => {
|
|||||||
game.move.use(MoveId.WISH, BattlerIndex.PLAYER_2);
|
game.move.use(MoveId.WISH, BattlerIndex.PLAYER_2);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expectWishActive();
|
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||||
|
|
||||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||||
game.move.use(MoveId.MEMENTO, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
game.move.use(MoveId.MEMENTO, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
// Wish went away without doing anything
|
// Wish went away without doing anything
|
||||||
expectWishActive(0);
|
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
|
||||||
expect(game.textInterceptor.logs).not.toContain(
|
expect(game.textInterceptor.logs).not.toContain(
|
||||||
i18next.t("arenaTag:wishTagOnAdd", {
|
i18next.t("arenaTag:wishTagOnAdd", {
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(blissey),
|
pokemonNameWithAffix: getPokemonNameWithAffix(blissey),
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,4 +1,5 @@
|
|||||||
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||||
|
import { receivedStr } from "#test/test-utils/test-utils";
|
||||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -14,22 +15,22 @@ export function toEqualArrayUnsorted(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!Array.isArray(received)) {
|
if (!Array.isArray(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected an array, but got ${this.utils.stringify(received)}!`,
|
message: () => `Expected to receive an array, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (received.length !== expected.length) {
|
if (received.length !== expected.length) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: false,
|
||||||
message: () => `Expected to receive array of length ${received.length}, but got ${expected.length} instead!`,
|
message: () => `Expected to receive an array of length ${received.length}, but got ${expected.length} instead!`,
|
||||||
actual: received,
|
|
||||||
expected,
|
expected,
|
||||||
|
actual: received,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const actualSorted = received.slice().sort();
|
const actualSorted = received.toSorted();
|
||||||
const expectedSorted = expected.slice().sort();
|
const expectedSorted = expected.toSorted();
|
||||||
const pass = this.equals(actualSorted, expectedSorted, [...this.customTesters, this.utils.iterableEquality]);
|
const pass = this.equals(actualSorted, expectedSorted, [...this.customTesters, this.utils.iterableEquality]);
|
||||||
|
|
||||||
const actualStr = getOnelineDiffStr.call(this, actualSorted);
|
const actualStr = getOnelineDiffStr.call(this, actualSorted);
|
||||||
|
@ -21,8 +21,8 @@ export function toHaveAbilityApplied(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to recieve a Pokemon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokemon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
77
test/test-utils/matchers/to-have-arena-tag.ts
Normal file
77
test/test-utils/matchers/to-have-arena-tag.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import type { ArenaTag, ArenaTagTypeMap } from "#data/arena-tag";
|
||||||
|
import type { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
|
import type { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
|
import type { OneOther } from "#test/@types/test-helpers";
|
||||||
|
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||||
|
import type { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||||
|
import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
// intersection required to preserve T for inferences
|
||||||
|
export type toHaveArenaTagOptions<T extends ArenaTagType> = OneOther<ArenaTagTypeMap[T], "tagType" | "side"> & {
|
||||||
|
tagType: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher to check if the {@linkcode Arena} has a given {@linkcode ArenaTag} active.
|
||||||
|
* @param received - The object to check. Should be the current {@linkcode GameManager}.
|
||||||
|
* @param expectedTag - The `ArenaTagType` of the desired tag, or a partially-filled object
|
||||||
|
* containing the desired properties
|
||||||
|
* @param side - The {@linkcode ArenaTagSide | side of the field} the tag should affect, or
|
||||||
|
* {@linkcode ArenaTagSide.BOTH} to check both sides
|
||||||
|
* @returns The result of the matching
|
||||||
|
*/
|
||||||
|
export function toHaveArenaTag<T extends ArenaTagType>(
|
||||||
|
this: MatcherState,
|
||||||
|
received: unknown,
|
||||||
|
expectedTag: T | toHaveArenaTagOptions<T>,
|
||||||
|
side?: ArenaTagSide,
|
||||||
|
): SyncExpectationResult {
|
||||||
|
if (!isGameManagerInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: this.isNot,
|
||||||
|
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!received.scene?.arena) {
|
||||||
|
return {
|
||||||
|
pass: this.isNot,
|
||||||
|
message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coerce lone `tagType`s into objects
|
||||||
|
// Bangs are ok as we enforce safety via overloads
|
||||||
|
// @ts-expect-error - Typescript is being stupid as tag type and side will always exist
|
||||||
|
const etag: Partial<ArenaTag> & { tagType: T; side: ArenaTagSide } =
|
||||||
|
typeof expectedTag === "object" ? expectedTag : { tagType: expectedTag, side: side! };
|
||||||
|
|
||||||
|
// We need to get all tags for the case of checking properties of a tag present on both sides of the arena
|
||||||
|
const tags = received.scene.arena.findTagsOnSide(t => t.tagType === etag.tagType, etag.side);
|
||||||
|
if (tags.length === 0) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected the Arena to have a tag of type ${etag.tagType}, but it didn't!`,
|
||||||
|
expected: etag.tagType,
|
||||||
|
actual: received.scene.arena.tags.map(t => t.tagType),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass if any of the matching tags meet our criteria
|
||||||
|
const pass = tags.some(tag =>
|
||||||
|
this.equals(tag, expectedTag, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const expectedStr = getOnelineDiffStr.call(this, expectedTag);
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected the Arena to NOT have a tag matching ${expectedStr}, but it did!`
|
||||||
|
: `Expected the Arena to have a tag matching ${expectedStr}, but it didn't!`,
|
||||||
|
expected: expectedTag,
|
||||||
|
actual: tags,
|
||||||
|
};
|
||||||
|
}
|
@ -6,7 +6,7 @@ import { getStatName } from "#test/test-utils/string-utils";
|
|||||||
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
export interface ToHaveEffectiveStatMatcherOptions {
|
export interface toHaveEffectiveStatOptions {
|
||||||
/**
|
/**
|
||||||
* The target {@linkcode Pokemon}
|
* The target {@linkcode Pokemon}
|
||||||
* @see {@linkcode Pokemon.getEffectiveStat}
|
* @see {@linkcode Pokemon.getEffectiveStat}
|
||||||
@ -30,7 +30,7 @@ export interface ToHaveEffectiveStatMatcherOptions {
|
|||||||
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
||||||
* @param stat - The {@linkcode EffectiveStat} to check
|
* @param stat - The {@linkcode EffectiveStat} to check
|
||||||
* @param expectedValue - The expected value of the {@linkcode stat}
|
* @param expectedValue - The expected value of the {@linkcode stat}
|
||||||
* @param options - The {@linkcode ToHaveEffectiveStatMatcherOptions}
|
* @param options - The {@linkcode toHaveEffectiveStatOptions}
|
||||||
* @returns Whether the matcher passed
|
* @returns Whether the matcher passed
|
||||||
*/
|
*/
|
||||||
export function toHaveEffectiveStat(
|
export function toHaveEffectiveStat(
|
||||||
@ -38,11 +38,11 @@ export function toHaveEffectiveStat(
|
|||||||
received: unknown,
|
received: unknown,
|
||||||
stat: EffectiveStat,
|
stat: EffectiveStat,
|
||||||
expectedValue: number,
|
expectedValue: number,
|
||||||
{ enemy, move, isCritical = false }: ToHaveEffectiveStatMatcherOptions = {},
|
{ enemy, move, isCritical = false }: toHaveEffectiveStatOptions = {},
|
||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
export function toHaveFainted(this: MatcherState, received: unknown): SyncExpectationResult {
|
export function toHaveFainted(this: MatcherState, received: unknown): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
export function toHaveFullHp(this: MatcherState, received: unknown): SyncExpectationResult {
|
export function toHaveFullHp(this: MatcherState, received: unknown): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
export function toHaveHp(this: MatcherState, received: unknown, expectedHp: number): SyncExpectationResult {
|
export function toHaveHp(this: MatcherState, received: unknown, expectedHp: number): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
107
test/test-utils/matchers/to-have-positional-tag.ts
Normal file
107
test/test-utils/matchers/to-have-positional-tag.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc
|
||||||
|
import type { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc
|
||||||
|
|
||||||
|
import type { serializedPosTagMap } from "#data/positional-tags/load-positional-tag";
|
||||||
|
import type { PositionalTagType } from "#enums/positional-tag-type";
|
||||||
|
import type { OneOther } from "#test/@types/test-helpers";
|
||||||
|
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||||
|
import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import { toTitleCase } from "#utils/strings";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
export type toHavePositionalTagOptions<P extends PositionalTagType> = OneOther<serializedPosTagMap[P], "tagType"> & {
|
||||||
|
tagType: P;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher to check if the {@linkcode Arena} has a certain number of {@linkcode PositionalTag}s active.
|
||||||
|
* @param received - The object to check. Should be the current {@linkcode GameManager}
|
||||||
|
* @param expectedTag - The {@linkcode PositionalTagType} of the desired tag, or a partially-filled {@linkcode PositionalTag}
|
||||||
|
* containing the desired properties
|
||||||
|
* @param count - The number of tags that should be active; defaults to `1` and must be within the range `[0, 4]`
|
||||||
|
* @returns The result of the matching
|
||||||
|
*/
|
||||||
|
export function toHavePositionalTag<P extends PositionalTagType>(
|
||||||
|
this: MatcherState,
|
||||||
|
received: unknown,
|
||||||
|
expectedTag: P | toHavePositionalTagOptions<P>,
|
||||||
|
count = 1,
|
||||||
|
): SyncExpectationResult {
|
||||||
|
if (!isGameManagerInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: this.isNot,
|
||||||
|
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!received.scene?.arena?.positionalTagManager) {
|
||||||
|
return {
|
||||||
|
pass: this.isNot,
|
||||||
|
message: () =>
|
||||||
|
`Expected GameManager.${received.scene?.arena ? "scene.arena.positionalTagManager" : received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Increase limit if triple battles are added
|
||||||
|
if (count < 0 || count > 4) {
|
||||||
|
return {
|
||||||
|
pass: this.isNot,
|
||||||
|
message: () => `Expected count to be between 0 and 4, but got ${count} instead!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const allTags = received.scene.arena.positionalTagManager.tags;
|
||||||
|
const tagType = typeof expectedTag === "string" ? expectedTag : expectedTag.tagType;
|
||||||
|
const matchingTags = allTags.filter(t => t.tagType === tagType);
|
||||||
|
|
||||||
|
// If checking exclusively tag type, check solely the number of matching tags on field
|
||||||
|
if (typeof expectedTag === "string") {
|
||||||
|
const pass = matchingTags.length === count;
|
||||||
|
const expectedStr = getPosTagStr(expectedTag);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected the Arena to NOT have ${count} ${expectedStr} active, but it did!`
|
||||||
|
: `Expected the Arena to have ${count} ${expectedStr} active, but got ${matchingTags.length} instead!`,
|
||||||
|
expected: expectedTag,
|
||||||
|
actual: allTags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for equality with the provided object
|
||||||
|
if (matchingTags.length === 0) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected the Arena to have a tag of type ${expectedTag.tagType}, but it didn't!`,
|
||||||
|
expected: expectedTag.tagType,
|
||||||
|
actual: received.scene.arena.tags.map(t => t.tagType),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass if any of the matching tags meet the criteria
|
||||||
|
const pass = matchingTags.some(tag =>
|
||||||
|
this.equals(tag, expectedTag, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const expectedStr = getOnelineDiffStr.call(this, expectedTag);
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected the Arena to NOT have a tag matching ${expectedStr}, but it did!`
|
||||||
|
: `Expected the Arena to have a tag matching ${expectedStr}, but it didn't!`,
|
||||||
|
expected: expectedTag,
|
||||||
|
actual: matchingTags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPosTagStr(pType: PositionalTagType, count = 1): string {
|
||||||
|
let ret = toTitleCase(pType) + "Tag";
|
||||||
|
if (count > 1) {
|
||||||
|
ret += "s";
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
@ -23,14 +23,14 @@ export function toHaveStatStage(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expectedStage < -6 || expectedStage > 6) {
|
if (expectedStage < -6 || expectedStage > 6) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected ${expectedStage} to be within the range [-6, 6]!`,
|
message: () => `Expected ${expectedStage} to be within the range [-6, 6]!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ export function toHaveStatusEffect(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -37,10 +37,8 @@ export function toHaveStatusEffect(
|
|||||||
const actualEffect = received.status?.effect ?? StatusEffect.NONE;
|
const actualEffect = received.status?.effect ?? StatusEffect.NONE;
|
||||||
|
|
||||||
// Check exclusively effect equality first, coercing non-matching status effects to numbers.
|
// Check exclusively effect equality first, coercing non-matching status effects to numbers.
|
||||||
if (actualEffect !== (expectedStatus as Exclude<typeof expectedStatus, StatusEffect>)?.effect) {
|
if (typeof expectedStatus === "object" && actualEffect !== expectedStatus.effect) {
|
||||||
// This is actually 100% safe as `expectedStatus?.effect` will evaluate to `undefined` if a StatusEffect was passed,
|
expectedStatus = expectedStatus.effect;
|
||||||
// which will never match actualEffect by definition
|
|
||||||
expectedStatus = (expectedStatus as Exclude<typeof expectedStatus, StatusEffect>).effect;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof expectedStatus === "number") {
|
if (typeof expectedStatus === "number") {
|
||||||
|
@ -24,7 +24,7 @@ export function toHaveTakenDamage(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,15 +20,15 @@ export function toHaveTerrain(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isGameManagerInstance(received)) {
|
if (!isGameManagerInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected GameManager, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!received.scene?.arena) {
|
if (!received.scene?.arena) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`,
|
message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,8 +41,8 @@ export function toHaveTerrain(
|
|||||||
pass,
|
pass,
|
||||||
message: () =>
|
message: () =>
|
||||||
pass
|
pass
|
||||||
? `Expected Arena to NOT have ${expectedStr} active, but it did!`
|
? `Expected the Arena to NOT have ${expectedStr} active, but it did!`
|
||||||
: `Expected Arena to have ${expectedStr} active, but got ${actualStr} instead!`,
|
: `Expected the Arena to have ${expectedStr} active, but got ${actualStr} instead!`,
|
||||||
expected: expectedTerrainType,
|
expected: expectedTerrainType,
|
||||||
actual,
|
actual,
|
||||||
};
|
};
|
||||||
|
@ -7,10 +7,16 @@ import { isPokemonInstance, receivedStr } from "../test-utils";
|
|||||||
|
|
||||||
export interface toHaveTypesOptions {
|
export interface toHaveTypesOptions {
|
||||||
/**
|
/**
|
||||||
* Whether to enforce exact matches (`true`) or superset matches (`false`).
|
* Value dictating the strength of the enforced typing match.
|
||||||
* @defaultValue `true`
|
*
|
||||||
|
* Possible values (in ascending order of strength) are:
|
||||||
|
* - `"ordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **and in the same order**
|
||||||
|
* - `"unordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **without checking order**
|
||||||
|
* - `"superset"`: Enforce that the {@linkcode Pokemon}'s types are **a superset of** the expected types
|
||||||
|
* (all must be present, but extras can be there)
|
||||||
|
* @defaultValue `"unordered"`
|
||||||
*/
|
*/
|
||||||
exact?: boolean;
|
mode?: "ordered" | "unordered" | "superset";
|
||||||
/**
|
/**
|
||||||
* Optional arguments to pass to {@linkcode Pokemon.getTypes}.
|
* Optional arguments to pass to {@linkcode Pokemon.getTypes}.
|
||||||
*/
|
*/
|
||||||
@ -18,35 +24,54 @@ export interface toHaveTypesOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matcher that checks if an array contains exactly the given items, disregarding order.
|
* Matcher that checks if a Pokemon's typing is as expected.
|
||||||
* @param received - The object to check. Should be an array of one or more {@linkcode PokemonType}s.
|
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
||||||
* @param options - The {@linkcode toHaveTypesOptions | options} for this matcher
|
* @param expectedTypes - An array of one or more {@linkcode PokemonType}s to compare against.
|
||||||
|
* @param mode - The mode to perform the matching in.
|
||||||
|
* Possible values (in ascending order of strength) are:
|
||||||
|
* - `"ordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **and in the same order**
|
||||||
|
* - `"unordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **without checking order**
|
||||||
|
* - `"superset"`: Enforce that the {@linkcode Pokemon}'s types are **a superset of** the expected types
|
||||||
|
* (all must be present, but extras can be there)
|
||||||
|
*
|
||||||
|
* Default `unordered`
|
||||||
|
* @param args - Extra arguments passed to {@linkcode Pokemon.getTypes}
|
||||||
* @returns The result of the matching
|
* @returns The result of the matching
|
||||||
*/
|
*/
|
||||||
export function toHaveTypes(
|
export function toHaveTypes(
|
||||||
this: MatcherState,
|
this: MatcherState,
|
||||||
received: unknown,
|
received: unknown,
|
||||||
expected: [PokemonType, ...PokemonType[]],
|
expectedTypes: [PokemonType, ...PokemonType[]],
|
||||||
options: toHaveTypesOptions = {},
|
{ mode = "unordered", args = [] }: toHaveTypesOptions = {},
|
||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to recieve a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const actualTypes = received.getTypes(...(options.args ?? [])).sort();
|
// Return early if no types were passed in
|
||||||
const expectedTypes = expected.slice().sort();
|
if (expectedTypes.length === 0) {
|
||||||
|
return {
|
||||||
|
pass: this.isNot,
|
||||||
|
message: () => "Expected to receive a non-empty array of PokemonTypes!",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid sorting the types if strict ordering is desired
|
||||||
|
const actualSorted = mode === "ordered" ? received.getTypes(...args) : received.getTypes(...args).toSorted();
|
||||||
|
const expectedSorted = mode === "ordered" ? expectedTypes : expectedTypes.toSorted();
|
||||||
|
|
||||||
// Exact matches do not care about subset equality
|
// Exact matches do not care about subset equality
|
||||||
const matchers = options.exact
|
const matchers =
|
||||||
|
mode === "superset"
|
||||||
? [...this.customTesters, this.utils.iterableEquality]
|
? [...this.customTesters, this.utils.iterableEquality]
|
||||||
: [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality];
|
: [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality];
|
||||||
const pass = this.equals(actualTypes, expectedTypes, matchers);
|
const pass = this.equals(actualSorted, expectedSorted, matchers);
|
||||||
|
|
||||||
const actualStr = stringifyEnumArray(PokemonType, actualTypes);
|
const actualStr = stringifyEnumArray(PokemonType, actualSorted);
|
||||||
const expectedStr = stringifyEnumArray(PokemonType, expectedTypes);
|
const expectedStr = stringifyEnumArray(PokemonType, expectedSorted);
|
||||||
const pkmName = getPokemonNameWithAffix(received);
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -55,7 +80,7 @@ export function toHaveTypes(
|
|||||||
pass
|
pass
|
||||||
? `Expected ${pkmName} to NOT have types ${expectedStr}, but it did!`
|
? `Expected ${pkmName} to NOT have types ${expectedStr}, but it did!`
|
||||||
: `Expected ${pkmName} to have types ${expectedStr}, but got ${actualStr} instead!`,
|
: `Expected ${pkmName} to have types ${expectedStr}, but got ${actualStr} instead!`,
|
||||||
expected: expectedTypes,
|
expected: expectedSorted,
|
||||||
actual: actualTypes,
|
actual: actualSorted,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
/**
|
/**
|
||||||
* Matcher to check the contents of a {@linkcode Pokemon}'s move history.
|
* Matcher to check the contents of a {@linkcode Pokemon}'s move history.
|
||||||
* @param received - The actual value received. Should be a {@linkcode Pokemon}
|
* @param received - The actual value received. Should be a {@linkcode Pokemon}
|
||||||
* @param expectedValue - The {@linkcode MoveId} the Pokemon is expected to have used,
|
* @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used,
|
||||||
* or a partially filled {@linkcode TurnMove} containing the desired properties to check
|
* or a partially filled {@linkcode TurnMove} containing the desired properties to check
|
||||||
* @param index - The index of the move history entry to check, in order from most recent to least recent.
|
* @param index - The index of the move history entry to check, in order from most recent to least recent.
|
||||||
* Default `0` (last used move)
|
* Default `0` (last used move)
|
||||||
@ -22,12 +22,12 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
export function toHaveUsedMove(
|
export function toHaveUsedMove(
|
||||||
this: MatcherState,
|
this: MatcherState,
|
||||||
received: unknown,
|
received: unknown,
|
||||||
expectedResult: MoveId | AtLeastOne<TurnMove>,
|
expectedMove: MoveId | AtLeastOne<TurnMove>,
|
||||||
index = 0,
|
index = 0,
|
||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -37,34 +37,33 @@ export function toHaveUsedMove(
|
|||||||
|
|
||||||
if (move === undefined) {
|
if (move === undefined) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected ${pkmName} to have used ${index + 1} moves, but it didn't!`,
|
message: () => `Expected ${pkmName} to have used ${index + 1} moves, but it didn't!`,
|
||||||
actual: received.getLastXMoves(-1),
|
actual: received.getLastXMoves(-1),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Coerce to a `TurnMove`
|
// Coerce to a `TurnMove`
|
||||||
if (typeof expectedResult === "number") {
|
if (typeof expectedMove === "number") {
|
||||||
expectedResult = { move: expectedResult };
|
expectedMove = { move: expectedMove };
|
||||||
}
|
}
|
||||||
|
|
||||||
const moveIndexStr = index === 0 ? "last move" : `${getOrdinal(index)} most recent move`;
|
const moveIndexStr = index === 0 ? "last move" : `${getOrdinal(index)} most recent move`;
|
||||||
|
|
||||||
const pass = this.equals(move, expectedResult, [
|
const pass = this.equals(move, expectedMove, [
|
||||||
...this.customTesters,
|
...this.customTesters,
|
||||||
this.utils.subsetEquality,
|
this.utils.subsetEquality,
|
||||||
this.utils.iterableEquality,
|
this.utils.iterableEquality,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const expectedStr = getOnelineDiffStr.call(this, expectedResult);
|
const expectedStr = getOnelineDiffStr.call(this, expectedMove);
|
||||||
return {
|
return {
|
||||||
pass,
|
pass,
|
||||||
message: () =>
|
message: () =>
|
||||||
pass
|
pass
|
||||||
? `Expected ${pkmName}'s ${moveIndexStr} to NOT match ${expectedStr}, but it did!`
|
? `Expected ${pkmName}'s ${moveIndexStr} to NOT match ${expectedStr}, but it did!`
|
||||||
: // Replace newlines with spaces to preserve one-line ness
|
: `Expected ${pkmName}'s ${moveIndexStr} to match ${expectedStr}, but it didn't!`,
|
||||||
`Expected ${pkmName}'s ${moveIndexStr} to match ${expectedStr}, but it didn't!`,
|
expected: expectedMove,
|
||||||
expected: expectedResult,
|
|
||||||
actual: move,
|
actual: move,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
/**
|
/**
|
||||||
* Matcher to check the amount of PP consumed by a {@linkcode Pokemon}.
|
* Matcher to check the amount of PP consumed by a {@linkcode Pokemon}.
|
||||||
* @param received - The actual value received. Should be a {@linkcode Pokemon}
|
* @param received - The actual value received. Should be a {@linkcode Pokemon}
|
||||||
* @param expectedValue - The {@linkcode MoveId} that should have consumed PP
|
* @param moveId - The {@linkcode MoveId} that should have consumed PP
|
||||||
* @param ppUsed - The numerical amount of PP that should have been consumed,
|
* @param ppUsed - The numerical amount of PP that should have been consumed,
|
||||||
* or `all` to indicate the move should be _out_ of PP
|
* or `all` to indicate the move should be _out_ of PP
|
||||||
* @returns Whether the matcher passed
|
* @returns Whether the matcher passed
|
||||||
@ -23,12 +23,12 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
export function toHaveUsedPP(
|
export function toHaveUsedPP(
|
||||||
this: MatcherState,
|
this: MatcherState,
|
||||||
received: unknown,
|
received: unknown,
|
||||||
expectedMove: MoveId,
|
moveId: MoveId,
|
||||||
ppUsed: number | "all",
|
ppUsed: number | "all",
|
||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -36,22 +36,22 @@ export function toHaveUsedPP(
|
|||||||
const override = received.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE;
|
const override = received.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE;
|
||||||
if (coerceArray(override).length > 0) {
|
if (coerceArray(override).length > 0) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () =>
|
message: () =>
|
||||||
`Cannot test for PP consumption with ${received.isPlayer() ? "player" : "enemy"} moveset overrides active!`,
|
`Cannot test for PP consumption with ${received.isPlayer() ? "player" : "enemy"} moveset overrides active!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const pkmName = getPokemonNameWithAffix(received);
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
const moveStr = getEnumStr(MoveId, expectedMove);
|
const moveStr = getEnumStr(MoveId, moveId);
|
||||||
|
|
||||||
const movesetMoves = received.getMoveset().filter(pm => pm.moveId === expectedMove);
|
const movesetMoves = received.getMoveset().filter(pm => pm.moveId === moveId);
|
||||||
if (movesetMoves.length !== 1) {
|
if (movesetMoves.length !== 1) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () =>
|
message: () =>
|
||||||
`Expected MoveId.${moveStr} to appear in ${pkmName}'s moveset exactly once, but got ${movesetMoves.length} times!`,
|
`Expected MoveId.${moveStr} to appear in ${pkmName}'s moveset exactly once, but got ${movesetMoves.length} times!`,
|
||||||
expected: expectedMove,
|
expected: moveId,
|
||||||
actual: received.getMoveset(),
|
actual: received.getMoveset(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,15 +20,15 @@ export function toHaveWeather(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isGameManagerInstance(received)) {
|
if (!isGameManagerInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected GameManager, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!received.scene?.arena) {
|
if (!received.scene?.arena) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`,
|
message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,8 +41,8 @@ export function toHaveWeather(
|
|||||||
pass,
|
pass,
|
||||||
message: () =>
|
message: () =>
|
||||||
pass
|
pass
|
||||||
? `Expected Arena to NOT have ${expectedStr} weather active, but it did!`
|
? `Expected the Arena to NOT have ${expectedStr} weather active, but it did!`
|
||||||
: `Expected Arena to have ${expectedStr} weather active, but got ${actualStr} instead!`,
|
: `Expected the Arena to have ${expectedStr} weather active, but got ${actualStr} instead!`,
|
||||||
expected: expectedWeatherType,
|
expected: expectedWeatherType,
|
||||||
actual,
|
actual,
|
||||||
};
|
};
|
||||||
|
@ -34,10 +34,10 @@ interface getEnumStrOptions {
|
|||||||
* @returns The stringified representation of `val` as dictated by the options.
|
* @returns The stringified representation of `val` as dictated by the options.
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* enum fakeEnum {
|
* enum testEnum {
|
||||||
* ONE: 1,
|
* ONE = 1,
|
||||||
* TWO: 2,
|
* TWO = 2,
|
||||||
* THREE: 3,
|
* THREE = 3,
|
||||||
* }
|
* }
|
||||||
* getEnumStr(fakeEnum, fakeEnum.ONE); // Output: "ONE (=1)"
|
* getEnumStr(fakeEnum, fakeEnum.ONE); // Output: "ONE (=1)"
|
||||||
* getEnumStr(fakeEnum, fakeEnum.TWO, {casing: "Title", prefix: "fakeEnum.", suffix: "!!!"}); // Output: "fakeEnum.TWO!!! (=2)"
|
* getEnumStr(fakeEnum, fakeEnum.TWO, {casing: "Title", prefix: "fakeEnum.", suffix: "!!!"}); // Output: "fakeEnum.TWO!!! (=2)"
|
||||||
@ -174,10 +174,14 @@ export function getStatName(s: Stat): string {
|
|||||||
* Convert an object into a oneline diff to be shown in an error message.
|
* Convert an object into a oneline diff to be shown in an error message.
|
||||||
* @param obj - The object to return the oneline diff of
|
* @param obj - The object to return the oneline diff of
|
||||||
* @returns The updated diff
|
* @returns The updated diff
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const diff = getOnelineDiffStr.call(this, obj)
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
export function getOnelineDiffStr(this: MatcherState, obj: unknown): string {
|
export function getOnelineDiffStr(this: MatcherState, obj: unknown): string {
|
||||||
return this.utils
|
return this.utils
|
||||||
.stringify(obj, undefined, { maxLength: 35, indent: 0, printBasicPrototype: false })
|
.stringify(obj, undefined, { maxLength: 35, indent: 0, printBasicPrototype: false })
|
||||||
.replace(/\n/g, " ") // Replace newlines with spaces
|
.replace(/\n/g, " ") // Replace newlines with spaces
|
||||||
.replace(/,(\s*)}$/g, "$1}");
|
.replace(/,(\s*)}$/g, "$1}"); // Trim trailing commas
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user