[Testing] Flexible Testing Wrapper for Phaser-Based Battle-Scenes (#1908)

* refactor executed code while importing and initializing all of these in loading-scene

* reset to main

* fix server url

* added rule no-trailing-spaces

* made progress

* test somme data from a session save is working

* trying to launch a battle

* added fetch wrapper to load data locally

* trying to mockAllSettled

* pushPhase & shiftPhase

* check integrity of exported session

* set toke + loggedInUser in tests

* progress on starting new battle

* tring to test phase but it's async

* mocking fetch

* working mock fetch

* need to handle pile of data

* attempt to use real phaser classes

* reorder overrides

* refactored to use some real classes from phaser

* removed useless things

* started to work on some container mock

* finished the mockContainer time to add some logic

* some more mock containers

* removed addMethods since there is the mock classes now

* commented issues

* attempt to create mockTextureManager

* fix tests

* mockSprite & mockText

* yes but not really

* yes but not really

* fix tutorial callback

* reached mode title

* added achievement tests

* fix test achievements with current state of mock

* correct sequence loading for BattleScene with mockLoader !

* deep dive into next step

* working wait until starter selection screen

* added newGame method into wrapper

* expect to save_slot

* trying to manage pokemon sprite for getAll without success yet

* added test for egg output

* fixed egg test for June

* fix tests + locate next issue to fix

* we are in battle baby

* added new game in one-line

* export is working but export only what's in the fetch

* fix start game as guest

* refactored how we start a battle + cleanup

* overrided mewtwo but issue with currentBattle

* refactor: rename InitAchievements to initAchievements

* added missing mock method

* override level and pokemon forms working as intended

* bringToTop Obj

* remove launch battle in achivement test

* fix getIndex when same pokemon

* can run all tests

* first attack, faint, and shop modifiers, MockClock

* on method for container

* added doAttack one-liner

* one-line export data

* removed throw error

* feat: Make `scenes` property of `GameWrapper` class public

The `scenes` property of the `GameWrapper` class was changed from private to public. This change allows external access to the `scenes` map, which is used to store Phaser scenes. This modification was made to enable easier manipulation and interaction with the scenes in the game.

* correction

* removed CanvasRenderer

* added a param to remove console.log and added a param to preven scene create call

* fix encounter wave 30 when it's a trainer

* test double-battle

* test fight without KO

* test double fight no ko

* fix crashing texture + added Text wrapper to log fight

* fix tests on boss - trainer - rival

* chore: Refactor BattleScene initialization and add new phases

Refactor the BattleScene initialization code to remove unnecessary delay and improve performance. Also, add new phases for the title and unavailable states to enhance the game experience.

* rework of Game tests

* skipFn is working

* added onNextPrompt and restore Og Start

* better newGame

* added skipFN in remove

* not yet working test but updated interceptors

* do attack work but not on PostSummonPhase phase when there is mention of silcoon and wurmple

* error located, it's just a double fight, i was not there yet

* single OHKO & double no OHKO

* added expirationFn into next prompt

* all tests are passing

* working test on non damaging move from opponent

* cleaned a bit

* removed phaser initialisation on every tests

* renamed test file

* added load system data

* added some ability support

* added onKill & onSummon abilities test

* removed useless test + cleanup

* removed useless test + cleanup

* fixed tests after merge main

* added itemHeld endTurn trigger test (toxic orb)

* added runFrom..To

* added mustRun to assert currentPhase

* added no-miss move to test things

* cleaner restore mock

* fix test

* fix moxie test + game speed

* improve test speed

* added onOurself and onOpponent mvoe test

* added onDamage test for tackle

* removed timeout in intervals to run tests faster

* cleanup

* added never crit override + separate file per test + remove randomness in randBattleSeedInt

* move folders

* better org

* renamed itemHeld folder to items

* fix deploy.yml

* cleanup

* simplified the gameManager start battle and allow single pokemon in party

* remove the need of mode development

* added input handler to test inputs + remove time from phaser into inputController

* added keyboard support

* added fakeMobile support

* added details

* removed a console.log + added logUp

* move test to folder

* fixed canvas issue

* added starter select tests

* added some more test on starter-select

* added battle-order tests

* added battle-order tests

* fixing Phaser RNG

* ordering stats for better reading

* fix tests for main

* adapt battle-order test to be more readable

* fix merge

* fix some errors and silent all errors from gameWrapper since it's not possible to avoid them

* fix mocks to manage childs & stuffs

* added some docs

* fix achievement test

* removed an unused file

* separate misc tests to clean battle.test file

* added a basic french lokalization test

* added i18n where it needs to be used only

* revers extracted method

* removed unused method

* removed handler fetch since we do not test anything server related

* fix test with handlers removed

* added intrepid sword test

* fix enum exp party

---------

Co-authored-by: Frederico Santos <frederico.f.santos@tecnico.ulisboa.pt>
This commit is contained in:
Greenlamp2 2024-06-08 00:33:45 +02:00 committed by GitHub
parent 3d87e86e58
commit 1a149bfa04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 4487 additions and 157 deletions

View File

@ -159,7 +159,7 @@ export default class BattleScene extends SceneBase {
public gameData: GameData; public gameData: GameData;
public sessionSlotId: integer; public sessionSlotId: integer;
private phaseQueue: Phase[]; public phaseQueue: Phase[];
private phaseQueuePrepend: Phase[]; private phaseQueuePrepend: Phase[];
private phaseQueuePrependSpliceIndex: integer; private phaseQueuePrependSpliceIndex: integer;
private nextCommandPhaseQueue: Phase[]; private nextCommandPhaseQueue: Phase[];
@ -201,7 +201,7 @@ export default class BattleScene extends SceneBase {
public arenaFlyout: ArenaFlyout; public arenaFlyout: ArenaFlyout;
private fieldOverlay: Phaser.GameObjects.Rectangle; private fieldOverlay: Phaser.GameObjects.Rectangle;
private modifiers: PersistentModifier[]; public modifiers: PersistentModifier[];
private enemyModifiers: PersistentModifier[]; private enemyModifiers: PersistentModifier[];
public uiContainer: Phaser.GameObjects.Container; public uiContainer: Phaser.GameObjects.Container;
public ui: UI; public ui: UI;
@ -300,7 +300,8 @@ export default class BattleScene extends SceneBase {
this.fieldSpritePipeline = new FieldSpritePipeline(this.game); this.fieldSpritePipeline = new FieldSpritePipeline(this.game);
(this.renderer as Phaser.Renderer.WebGL.WebGLRenderer).pipelines.add("FieldSprite", this.fieldSpritePipeline); (this.renderer as Phaser.Renderer.WebGL.WebGLRenderer).pipelines.add("FieldSprite", this.fieldSpritePipeline);
this.time.delayedCall(20, () => this.launchBattle());
this.launchBattle();
} }
update() { update() {
@ -947,7 +948,8 @@ export default class BattleScene extends SceneBase {
} }
newBattle(waveIndex?: integer, battleType?: BattleType, trainerData?: TrainerData, double?: boolean): Battle { newBattle(waveIndex?: integer, battleType?: BattleType, trainerData?: TrainerData, double?: boolean): Battle {
const newWaveIndex = waveIndex || ((this.currentBattle?.waveIndex || (startingWave - 1)) + 1); const _startingWave = Overrides.STARTING_WAVE_OVERRIDE || startingWave;
const newWaveIndex = waveIndex || ((this.currentBattle?.waveIndex || (_startingWave - 1)) + 1);
let newDouble: boolean; let newDouble: boolean;
let newBattleType: BattleType; let newBattleType: BattleType;
let newTrainer: Trainer; let newTrainer: Trainer;
@ -1007,6 +1009,9 @@ export default class BattleScene extends SceneBase {
if (Overrides.DOUBLE_BATTLE_OVERRIDE) { if (Overrides.DOUBLE_BATTLE_OVERRIDE) {
newDouble = true; newDouble = true;
} }
if (Overrides.SINGLE_BATTLE_OVERRIDE) {
newDouble = false;
}
const lastBattle = this.currentBattle; const lastBattle = this.currentBattle;

View File

@ -1727,6 +1727,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
const critChance = [24, 8, 2, 1][Math.max(0, Math.min(critLevel.value, 3))]; const critChance = [24, 8, 2, 1][Math.max(0, Math.min(critLevel.value, 3))];
isCritical = !source.getTag(BattlerTagType.NO_CRIT) && (critChance === 1 || !this.scene.randBattleSeedInt(critChance)); isCritical = !source.getTag(BattlerTagType.NO_CRIT) && (critChance === 1 || !this.scene.randBattleSeedInt(critChance));
if (Overrides.NEVER_CRIT_OVERRIDE) {
isCritical = false;
}
} }
if (isCritical) { if (isCritical) {
const blockCrit = new Utils.BooleanHolder(false); const blockCrit = new Utils.BooleanHolder(false);

View File

@ -94,7 +94,6 @@ export class InputsController {
private buttonLock: Button; private buttonLock: Button;
private interactions: Map<Button, Map<string, boolean>> = new Map(); private interactions: Map<Button, Map<string, boolean>> = new Map();
private time: Phaser.Time.Clock;
private configs: Map<string, InterfaceConfig> = new Map(); private configs: Map<string, InterfaceConfig> = new Map();
public gamepadSupport: boolean = true; public gamepadSupport: boolean = true;
@ -121,7 +120,6 @@ export class InputsController {
constructor(scene: BattleScene) { constructor(scene: BattleScene) {
this.scene = scene; this.scene = scene;
this.time = this.scene.time;
this.selectedDevice = { this.selectedDevice = {
[Device.GAMEPAD]: null, [Device.GAMEPAD]: null,
[Device.KEYBOARD]: "default" [Device.KEYBOARD]: "default"
@ -246,6 +244,9 @@ export class InputsController {
* If an interaction is valid and should be processed, it emits an 'input_down' event with details of the interaction. * If an interaction is valid and should be processed, it emits an 'input_down' event with details of the interaction.
*/ */
update(): void { update(): void {
if (this.pauseUpdate) {
return;
}
for (const b of Utils.getEnumValues(Button).reverse()) { for (const b of Utils.getEnumValues(Button).reverse()) {
if ( if (
this.interactions.hasOwnProperty(b) && this.interactions.hasOwnProperty(b) &&
@ -256,8 +257,7 @@ export class InputsController {
if ( if (
(!this.gamepadSupport && this.interactions[b].source === "gamepad") || (!this.gamepadSupport && this.interactions[b].source === "gamepad") ||
(this.interactions[b].source === "gamepad" && this.interactions[b].sourceName && this.interactions[b].sourceName !== this.selectedDevice[Device.GAMEPAD]) || (this.interactions[b].source === "gamepad" && this.interactions[b].sourceName && this.interactions[b].sourceName !== this.selectedDevice[Device.GAMEPAD]) ||
(this.interactions[b].source === "keyboard" && this.interactions[b].sourceName && this.interactions[b].sourceName !== this.selectedDevice[Device.KEYBOARD]) || (this.interactions[b].source === "keyboard" && this.interactions[b].sourceName && this.interactions[b].sourceName !== this.selectedDevice[Device.KEYBOARD])
this.pauseUpdate
) { ) {
// Deletes the last interaction for a button if gamepad is disabled. // Deletes the last interaction for a button if gamepad is disabled.
this.delLastProcessedMovementTime(b as Button); this.delLastProcessedMovementTime(b as Button);
@ -548,7 +548,8 @@ export class InputsController {
if (!this.isButtonLocked(button)) { if (!this.isButtonLocked(button)) {
return false; return false;
} }
if (this.time.now - this.interactions[button].pressTime >= repeatInputDelayMillis) { const duration = Date.now() - this.interactions[button].pressTime;
if (duration >= repeatInputDelayMillis) {
return true; return true;
} }
} }
@ -573,7 +574,7 @@ export class InputsController {
return; return;
} }
this.setButtonLock(button); this.setButtonLock(button);
this.interactions[button].pressTime = this.time.now; this.interactions[button].pressTime = Date.now();
this.interactions[button].isPressed = true; this.interactions[button].isPressed = true;
this.interactions[button].source = source; this.interactions[button].source = source;
this.interactions[button].sourceName = sourceName.toLowerCase(); this.interactions[button].sourceName = sourceName.toLowerCase();
@ -633,7 +634,7 @@ export class InputsController {
this.interactions[b].sourceName = null; this.interactions[b].sourceName = null;
} }
} }
setTimeout(() => this.pauseUpdate = false, 500); this.pauseUpdate = false;
} }
/** /**

View File

@ -16,6 +16,7 @@ import {initPokemonForms} from "#app/data/pokemon-forms";
import {initSpecies} from "#app/data/pokemon-species"; import {initSpecies} from "#app/data/pokemon-species";
import {initMoves} from "#app/data/move"; import {initMoves} from "#app/data/move";
import {initAbilities} from "#app/data/ability"; import {initAbilities} from "#app/data/ability";
import {initAchievements} from "#app/system/achv";
import {initTrainerTypeDialogue} from "#app/data/dialogue"; import {initTrainerTypeDialogue} from "#app/data/dialogue";
import i18next from "i18next"; import i18next from "i18next";
import { initStatsKeys } from "./ui/game-stats-ui-handler"; import { initStatsKeys } from "./ui/game-stats-ui-handler";
@ -328,6 +329,7 @@ export class LoadingScene extends SceneBase {
this.loadLoadingScreen(); this.loadLoadingScreen();
initAchievements();
initStatsKeys(); initStatsKeys();
initPokemonPrevolutions(); initPokemonPrevolutions();
initBiomes(); initBiomes();

View File

@ -29,6 +29,7 @@ import { modifierTypes } from "./modifier/modifier-type";
export const SEED_OVERRIDE: string = ""; export const SEED_OVERRIDE: string = "";
export const WEATHER_OVERRIDE: WeatherType = WeatherType.NONE; export const WEATHER_OVERRIDE: WeatherType = WeatherType.NONE;
export const DOUBLE_BATTLE_OVERRIDE: boolean = false; export const DOUBLE_BATTLE_OVERRIDE: boolean = false;
export const SINGLE_BATTLE_OVERRIDE: boolean = false;
export const STARTING_WAVE_OVERRIDE: integer = 0; export const STARTING_WAVE_OVERRIDE: integer = 0;
export const STARTING_BIOME_OVERRIDE: Biome = Biome.TOWN; export const STARTING_BIOME_OVERRIDE: Biome = Biome.TOWN;
export const ARENA_TINT_OVERRIDE: TimeOfDay = null; export const ARENA_TINT_OVERRIDE: TimeOfDay = null;
@ -110,6 +111,7 @@ export const OPP_MODIFIER_OVERRIDE: Array<ModifierOverride> = [];
export const STARTING_HELD_ITEMS_OVERRIDE: Array<ModifierOverride> = []; export const STARTING_HELD_ITEMS_OVERRIDE: Array<ModifierOverride> = [];
export const OPP_HELD_ITEMS_OVERRIDE: Array<ModifierOverride> = []; export const OPP_HELD_ITEMS_OVERRIDE: Array<ModifierOverride> = [];
export const NEVER_CRIT_OVERRIDE: boolean = false;
/** /**
* An array of items by keys as defined in the "modifierTypes" object in the "modifier/modifier-type.ts" file. * An array of items by keys as defined in the "modifierTypes" object in the "modifier/modifier-type.ts" file.

View File

@ -149,7 +149,7 @@ export class LoginPhase extends Phase {
export class TitlePhase extends Phase { export class TitlePhase extends Phase {
private loaded: boolean; private loaded: boolean;
private lastSessionData: SessionSaveData; private lastSessionData: SessionSaveData;
private gameMode: GameModes; public gameMode: GameModes;
constructor(scene: BattleScene) { constructor(scene: BattleScene) {
super(scene); super(scene);
@ -527,7 +527,12 @@ export class SelectStarterPhase extends Phase {
return this.end(); return this.end();
} }
this.scene.sessionSlotId = slotId; this.scene.sessionSlotId = slotId;
this.initBattle(starters);
});
}, this.gameMode);
}
initBattle(starters: Starter[]) {
const party = this.scene.getParty(); const party = this.scene.getParty();
const loadPokemonAssets: Promise<void>[] = []; const loadPokemonAssets: Promise<void>[] = [];
starters.forEach((starter: Starter, i: integer) => { starters.forEach((starter: Starter, i: integer) => {
@ -578,8 +583,6 @@ export class SelectStarterPhase extends Phase {
this.scene.lastSavePlayTime = 0; this.scene.lastSavePlayTime = 0;
this.end(); this.end();
}); });
});
}, this.gameMode);
} }
} }

View File

@ -40,6 +40,10 @@ export class Achv {
return i18next.t(`achv:${this.localizationKey}.name`); return i18next.t(`achv:${this.localizationKey}.name`);
} }
getDescription(): string {
return this.description;
}
getIconImage(): string { getIconImage(): string {
return this.iconImage; return this.iconImage;
} }
@ -259,8 +263,7 @@ export const achvs = {
CLASSIC_VICTORY: new Achv("CLASSIC_VICTORY","", "CLASSIC_VICTORY.description", "relic_crown", 150), CLASSIC_VICTORY: new Achv("CLASSIC_VICTORY","", "CLASSIC_VICTORY.description", "relic_crown", 150),
}; };
{ export function initAchievements() {
(function() {
const achvKeys = Object.keys(achvs); const achvKeys = Object.keys(achvs);
achvKeys.forEach((a: string, i: integer) => { achvKeys.forEach((a: string, i: integer) => {
achvs[a].id = a; achvs[a].id = a;
@ -268,5 +271,4 @@ export const achvs = {
achvs[a].parentId = achvKeys[i - 1]; achvs[a].parentId = achvKeys[i - 1];
} }
}); });
})();
} }

View File

@ -60,13 +60,13 @@ export function getDataTypeKey(dataType: GameDataType, slotId: integer = 0): str
} }
} }
function encrypt(data: string, bypassLogin: boolean): string { export function encrypt(data: string, bypassLogin: boolean): string {
return (bypassLogin return (bypassLogin
? (data: string) => btoa(data) ? (data: string) => btoa(data)
: (data: string) => AES.encrypt(data, saveKey))(data); : (data: string) => AES.encrypt(data, saveKey))(data);
} }
function decrypt(data: string, bypassLogin: boolean): string { export function decrypt(data: string, bypassLogin: boolean): string {
return (bypassLogin return (bypassLogin
? (data: string) => atob(data) ? (data: string) => atob(data)
: (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8))(data); : (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8))(data);
@ -493,7 +493,7 @@ export class GameData {
}); });
} }
private parseSystemData(dataStr: string): SystemSaveData { parseSystemData(dataStr: string): SystemSaveData {
return JSON.parse(dataStr, (k: string, v: any) => { return JSON.parse(dataStr, (k: string, v: any) => {
if (k === "gameStats") { if (k === "gameStats") {
return new GameStats(v); return new GameStats(v);
@ -512,7 +512,7 @@ export class GameData {
}) as SystemSaveData; }) as SystemSaveData;
} }
private convertSystemDataStr(dataStr: string, shorten: boolean = false): string { convertSystemDataStr(dataStr: string, shorten: boolean = false): string {
if (!shorten) { if (!shorten) {
// Account for past key oversight // Account for past key oversight
dataStr = dataStr.replace(/\$pAttr/g, "$pa"); dataStr = dataStr.replace(/\$pAttr/g, "$pa");

View File

@ -0,0 +1,83 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import * as overrides from "#app/overrides";
import {Abilities} from "#app/data/enums/abilities";
import {Species} from "#app/data/enums/species";
import {
CheckSwitchPhase, CommandPhase, MessagePhase,
PostSummonPhase,
ShinySparklePhase,
ShowAbilityPhase,
StatChangePhase,
SummonPhase,
ToggleDoublePositionPhase, TurnInitPhase
} from "#app/phases";
import {Mode} from "#app/ui/ui";
import {BattleStat} from "#app/data/battle-stat";
describe("Abilities - Intimidate", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MIGHTYENA);
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INTIMIDATE);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INTIMIDATE);
});
it("INTIMIDATE", async() => {
await game.runToSummon([
Species.MIGHTYENA,
Species.MIGHTYENA,
]);
await game.phaseInterceptor.run(PostSummonPhase);
expect(game.scene.getParty()[0].summonData).not.toBeUndefined();
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(0);
await game.phaseInterceptor.run(ShowAbilityPhase);
await game.phaseInterceptor.run(StatChangePhase);
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
await game.phaseInterceptor.run(SummonPhase);
await game.phaseInterceptor.run(ShinySparklePhase, () => game.isCurrentPhase(ToggleDoublePositionPhase));
await game.phaseInterceptor.run(ToggleDoublePositionPhase);
game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => {
game.setMode(Mode.MESSAGE);
game.endPhase();
});
await game.phaseInterceptor.run(CheckSwitchPhase);
await game.phaseInterceptor.run(PostSummonPhase);
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
await game.phaseInterceptor.run(ShowAbilityPhase);
game.scene.moveAnimations = null; // Mandatory to avoid crash
await game.phaseInterceptor.run(StatChangePhase);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
await game.phaseInterceptor.run(MessagePhase);
await game.phaseInterceptor.run(TurnInitPhase);
await game.phaseInterceptor.run(CommandPhase);
}, 20000);
});

View File

@ -0,0 +1,65 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import * as overrides from "#app/overrides";
import {Abilities} from "#app/data/enums/abilities";
import {Species} from "#app/data/enums/species";
import {
MessagePhase,
PostSummonPhase,
ShowAbilityPhase,
StatChangePhase,
ToggleDoublePositionPhase
} from "#app/phases";
import {BattleStat} from "#app/data/battle-stat";
describe("Abilities - Intrepid Sword", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.ZACIAN);
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INTREPID_SWORD);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INTREPID_SWORD);
});
it("INTREPID SWORD on player", async() => {
await game.runToSummon([
Species.ZACIAN,
]);
await game.phaseInterceptor.runFrom(PostSummonPhase).to(PostSummonPhase);
expect(game.scene.getParty()[0].summonData).not.toBeUndefined();
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(0);
await game.phaseInterceptor.mustRun(ShowAbilityPhase).catch((error) => expect(error).toBe(ShowAbilityPhase));
await game.phaseInterceptor.mustRun(StatChangePhase).catch((error) => expect(error).toBe(StatChangePhase));
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(1);
}, 20000);
it("INTREPID SWORD on opponent", async() => {
await game.runToSummon([
Species.ZACIAN,
]);
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
await game.phaseInterceptor.runFrom(PostSummonPhase).to(ToggleDoublePositionPhase);
await game.phaseInterceptor.mustRun(StatChangePhase).catch((error) => expect(error).toBe(StatChangePhase));
await game.phaseInterceptor.whenAboutToRun(MessagePhase);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(1);
}, 20000);
});

View File

@ -0,0 +1,67 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import * as overrides from "#app/overrides";
import {Abilities} from "#app/data/enums/abilities";
import {Species} from "#app/data/enums/species";
import {
CommandPhase,
EnemyCommandPhase,
VictoryPhase
} from "#app/phases";
import {Mode} from "#app/ui/ui";
import {Stat} from "#app/data/pokemon-stat";
import {Moves} from "#app/data/enums/moves";
import {getMovePosition} from "#app/test/utils/gameManagerUtils";
import {Command} from "#app/ui/command-ui-handler";
import {BattleStat} from "#app/data/battle-stat";
describe("Abilities - Moxie", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
const moveToUse = Moves.AERIAL_ACE;
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA);
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.MOXIE);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.MOXIE);
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(2000);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]);
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]);
});
it("MOXIE", async() => {
const moveToUse = Moves.AERIAL_ACE;
await game.startBattle([
Species.MIGHTYENA,
Species.MIGHTYENA,
]);
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[Stat.ATK]).toBe(0);
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
const movePosition = getMovePosition(game.scene, 0, moveToUse);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase);
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(1);
}, 20000);
});

View File

@ -1,9 +0,0 @@
import { MoneyAchv } from "#app/system/achv";
import { describe, expect, it } from "vitest";
describe("check some Achievement related stuff", () => {
it ("should check Achievement creation", () => {
const ach = new MoneyAchv("", "Achievement", 1000, null, 100);
expect(ach.name).toBe("Achievement");
});
});

View File

@ -0,0 +1,274 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import {MoneyAchv, Achv, AchvTier, RibbonAchv, DamageAchv, HealAchv, LevelAchv, ModifierAchv, achvs} from "#app/system/achv";
import BattleScene from "../../battle-scene";
import { IntegerHolder, NumberHolder } from "#app/utils.js";
import { TurnHeldItemTransferModifier } from "#app/modifier/modifier.js";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import * as overrides from "#app/overrides";
describe("check some Achievement related stuff", () => {
it ("should check Achievement creation", () => {
const ach = new MoneyAchv("", "Achievement", 1000, null, 100);
expect(ach.name).toBe("Achievement");
});
});
describe("Achv", () => {
let achv: Achv;
beforeEach(() => {
achv = new Achv("", "Test Achievement", "This is a test achievement", "test_icon", 10);
});
it("should have the correct name", () => {
expect(achv.getDescription()).toBe("This is a test achievement");
});
it("should have the correct icon image", () => {
expect(achv.getIconImage()).toBe("test_icon");
});
it("should set the achievement as secret", () => {
achv.setSecret();
expect(achv.secret).toBe(true);
expect(achv.hasParent).toBe(false);
achv.setSecret(true);
expect(achv.secret).toBe(true);
expect(achv.hasParent).toBe(true);
achv.setSecret(false);
expect(achv.secret).toBe(true);
expect(achv.hasParent).toBe(false);
});
it("should return the correct tier based on the score", () => {
const achv1 = new Achv("", "Test Achievement 1", "Test Description", "test_icon", 10);
const achv2 = new Achv("", "Test Achievement 2", "Test Description", "test_icon", 25);
const achv3 = new Achv("", "Test Achievement 3", "Test Description", "test_icon", 50);
const achv4 = new Achv("", "Test Achievement 4", "Test Description", "test_icon", 75);
const achv5 = new Achv("", "Test Achievement 5", "Test Description", "test_icon", 100);
expect(achv1.getTier()).toBe(AchvTier.COMMON);
expect(achv2.getTier()).toBe(AchvTier.GREAT);
expect(achv3.getTier()).toBe(AchvTier.ULTRA);
expect(achv4.getTier()).toBe(AchvTier.ROGUE);
expect(achv5.getTier()).toBe(AchvTier.MASTER);
});
it("should validate the achievement based on the condition function", () => {
const conditionFunc = jest.fn((scene: BattleScene, args: any[]) => args[0] === 10);
const achv = new Achv("", "Test Achievement", "Test Description", "test_icon", 10, conditionFunc);
expect(achv.validate(new BattleScene(), [5])).toBe(false);
expect(achv.validate(new BattleScene(), [10])).toBe(true);
expect(conditionFunc).toHaveBeenCalledTimes(2);
});
});
describe("MoneyAchv", () => {
it("should create an instance of MoneyAchv", () => {
const moneyAchv = new MoneyAchv("", "Test Money Achievement", 10000, "money_icon", 10);
expect(moneyAchv).toBeInstanceOf(MoneyAchv);
expect(moneyAchv instanceof Achv).toBe(true);
});
it("should validate the achievement based on the money amount", () => {
const moneyAchv = new MoneyAchv("", "Test Money Achievement", 10000, "money_icon", 10);
const scene = new BattleScene();
scene.money = 5000;
expect(moneyAchv.validate(scene, [])).toBe(false);
scene.money = 15000;
expect(moneyAchv.validate(scene, [])).toBe(true);
});
});
describe("RibbonAchv", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
let scene: BattleScene;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(0);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(0);
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(0);
vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(0);
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([]);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([]);
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false);
vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(false);
game = new GameManager(phaserGame);
scene = game.scene;
});
it("should create an instance of RibbonAchv", () => {
const ribbonAchv = new RibbonAchv("", "Test Ribbon Achievement", 10, "ribbon_icon", 10);
expect(ribbonAchv).toBeInstanceOf(RibbonAchv);
expect(ribbonAchv instanceof Achv).toBe(true);
});
it("should validate the achievement based on the ribbon amount", () => {
const ribbonAchv = new RibbonAchv("", "Test Ribbon Achievement", 10, "ribbon_icon", 10);
scene.gameData.gameStats.ribbonsOwned = 5;
expect(ribbonAchv.validate(scene, [])).toBe(false);
scene.gameData.gameStats.ribbonsOwned = 15;
expect(ribbonAchv.validate(scene, [])).toBe(true);
});
});
describe("DamageAchv", () => {
it("should create an instance of DamageAchv", () => {
const damageAchv = new DamageAchv("", "Test Damage Achievement", 250, "damage_icon", 10);
expect(damageAchv).toBeInstanceOf(DamageAchv);
expect(damageAchv instanceof Achv).toBe(true);
});
it("should validate the achievement based on the damage amount", () => {
const damageAchv = new DamageAchv("", "Test Damage Achievement", 250, "damage_icon", 10);
const scene = new BattleScene();
const numberHolder = new NumberHolder(200);
expect(damageAchv.validate(scene, [numberHolder])).toBe(false);
numberHolder.value = 300;
expect(damageAchv.validate(scene, [numberHolder])).toBe(true);
});
});
describe("HealAchv", () => {
it("should create an instance of HealAchv", () => {
const healAchv = new HealAchv("", "Test Heal Achievement", 250, "heal_icon", 10);
expect(healAchv).toBeInstanceOf(HealAchv);
expect(healAchv instanceof Achv).toBe(true);
});
it("should validate the achievement based on the heal amount", () => {
const healAchv = new HealAchv("", "Test Heal Achievement", 250, "heal_icon", 10);
const scene = new BattleScene();
const numberHolder = new NumberHolder(200);
expect(healAchv.validate(scene, [numberHolder])).toBe(false);
numberHolder.value = 300;
expect(healAchv.validate(scene, [numberHolder])).toBe(true);
});
});
describe("LevelAchv", () => {
it("should create an instance of LevelAchv", () => {
const levelAchv = new LevelAchv("", "Test Level Achievement", 100, "level_icon", 10);
expect(levelAchv).toBeInstanceOf(LevelAchv);
expect(levelAchv instanceof Achv).toBe(true);
});
it("should validate the achievement based on the level", () => {
const levelAchv = new LevelAchv("", "Test Level Achievement", 100, "level_icon", 10);
const scene = new BattleScene();
const integerHolder = new IntegerHolder(50);
expect(levelAchv.validate(scene, [integerHolder])).toBe(false);
integerHolder.value = 150;
expect(levelAchv.validate(scene, [integerHolder])).toBe(true);
});
});
describe("ModifierAchv", () => {
it("should create an instance of ModifierAchv", () => {
const modifierAchv = new ModifierAchv("", "Test Modifier Achievement", "Test Description", "modifier_icon", 10, () => true);
expect(modifierAchv).toBeInstanceOf(ModifierAchv);
expect(modifierAchv instanceof Achv).toBe(true);
});
it("should validate the achievement based on the modifier function", () => {
const modifierAchv = new ModifierAchv("", "Test Modifier Achievement", "Test Description", "modifier_icon", 10, () => true);
const scene = new BattleScene();
const modifier = new TurnHeldItemTransferModifier(null, 3, 1);
expect(modifierAchv.validate(scene, [modifier])).toBe(true);
});
});
describe("achvs", () => {
it("should contain the predefined achievements", () => {
expect(achvs._10K_MONEY).toBeInstanceOf(MoneyAchv);
expect(achvs._100K_MONEY).toBeInstanceOf(MoneyAchv);
expect(achvs._1M_MONEY).toBeInstanceOf(MoneyAchv);
expect(achvs._10M_MONEY).toBeInstanceOf(MoneyAchv);
expect(achvs._250_DMG).toBeInstanceOf(DamageAchv);
expect(achvs._1000_DMG).toBeInstanceOf(DamageAchv);
expect(achvs._2500_DMG).toBeInstanceOf(DamageAchv);
expect(achvs._10000_DMG).toBeInstanceOf(DamageAchv);
expect(achvs._250_HEAL).toBeInstanceOf(HealAchv);
expect(achvs._1000_HEAL).toBeInstanceOf(HealAchv);
expect(achvs._2500_HEAL).toBeInstanceOf(HealAchv);
expect(achvs._10000_HEAL).toBeInstanceOf(HealAchv);
expect(achvs.LV_100).toBeInstanceOf(LevelAchv);
expect(achvs.LV_250).toBeInstanceOf(LevelAchv);
expect(achvs.LV_1000).toBeInstanceOf(LevelAchv);
expect(achvs._10_RIBBONS).toBeInstanceOf(RibbonAchv);
expect(achvs._25_RIBBONS).toBeInstanceOf(RibbonAchv);
expect(achvs._50_RIBBONS).toBeInstanceOf(RibbonAchv);
expect(achvs._75_RIBBONS).toBeInstanceOf(RibbonAchv);
expect(achvs._100_RIBBONS).toBeInstanceOf(RibbonAchv);
expect(achvs.TRANSFER_MAX_BATTLE_STAT).toBeInstanceOf(Achv);
expect(achvs.MAX_FRIENDSHIP).toBeInstanceOf(Achv);
expect(achvs.MEGA_EVOLVE).toBeInstanceOf(Achv);
expect(achvs.GIGANTAMAX).toBeInstanceOf(Achv);
expect(achvs.TERASTALLIZE).toBeInstanceOf(Achv);
expect(achvs.STELLAR_TERASTALLIZE).toBeInstanceOf(Achv);
expect(achvs.SPLICE).toBeInstanceOf(Achv);
expect(achvs.MINI_BLACK_HOLE).toBeInstanceOf(ModifierAchv);
expect(achvs.CATCH_MYTHICAL).toBeInstanceOf(Achv);
expect(achvs.CATCH_SUB_LEGENDARY).toBeInstanceOf(Achv);
expect(achvs.CATCH_LEGENDARY).toBeInstanceOf(Achv);
expect(achvs.SEE_SHINY).toBeInstanceOf(Achv);
expect(achvs.SHINY_PARTY).toBeInstanceOf(Achv);
expect(achvs.HATCH_MYTHICAL).toBeInstanceOf(Achv);
expect(achvs.HATCH_SUB_LEGENDARY).toBeInstanceOf(Achv);
expect(achvs.HATCH_LEGENDARY).toBeInstanceOf(Achv);
expect(achvs.HATCH_SHINY).toBeInstanceOf(Achv);
expect(achvs.HIDDEN_ABILITY).toBeInstanceOf(Achv);
expect(achvs.PERFECT_IVS).toBeInstanceOf(Achv);
expect(achvs.CLASSIC_VICTORY).toBeInstanceOf(Achv);
});
it("should initialize the achievements with IDs and parent IDs", () => {
expect(achvs._10K_MONEY.id).toBe("_10K_MONEY");
expect(achvs._10K_MONEY.hasParent).toBe(undefined);
expect(achvs._100K_MONEY.id).toBe("_100K_MONEY");
expect(achvs._100K_MONEY.hasParent).toBe(true);
expect(achvs._100K_MONEY.parentId).toBe("_10K_MONEY");
expect(achvs._1M_MONEY.id).toBe("_1M_MONEY");
expect(achvs._1M_MONEY.hasParent).toBe(true);
expect(achvs._1M_MONEY.parentId).toBe("_100K_MONEY");
expect(achvs._10M_MONEY.id).toBe("_10M_MONEY");
expect(achvs._10M_MONEY.hasParent).toBe(true);
expect(achvs._10M_MONEY.parentId).toBe("_1M_MONEY");
expect(achvs.LV_100.id).toBe("LV_100");
expect(achvs.LV_100.hasParent).toBe(false);
expect(achvs.LV_250.id).toBe("LV_250");
expect(achvs.LV_250.hasParent).toBe(true);
expect(achvs.LV_250.parentId).toBe("LV_100");
expect(achvs.LV_1000.id).toBe("LV_1000");
expect(achvs.LV_1000.hasParent).toBe(true);
expect(achvs.LV_1000.parentId).toBe("LV_250");
});
});

View File

@ -0,0 +1,220 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import * as overrides from "#app/overrides";
import {Abilities} from "#app/data/enums/abilities";
import {Species} from "#app/data/enums/species";
import {
CommandPhase, EnemyCommandPhase,
TurnStartPhase
} from "#app/phases";
import {Mode} from "#app/ui/ui";
import {getMovePosition} from "#app/test/utils/gameManagerUtils";
import {Moves} from "#app/data/enums/moves";
import {Command} from "#app/ui/command-ui-handler";
import {Stat} from "#app/data/pokemon-stat";
import TargetSelectUiHandler from "#app/ui/target-select-ui-handler";
import {Button} from "#app/enums/buttons";
describe("Battle order", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MEWTWO);
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INSOMNIA);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INSOMNIA);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
});
it("opponent faster than player 50 vs 150", async() => {
await game.startBattle([
Species.BULBASAUR,
]);
game.scene.getParty()[0].stats[Stat.SPD] = 50;
game.scene.currentBattle.enemyParty[0].stats[Stat.SPD] = 150;
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
const movePosition = getMovePosition(game.scene, 0, Moves.TACKLE);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
await game.phaseInterceptor.run(EnemyCommandPhase);
await game.phaseInterceptor.whenAboutToRun(TurnStartPhase);
const phase = game.scene.getCurrentPhase() as TurnStartPhase;
const order = phase.getOrder();
expect(order[0]).toBe(2);
expect(order[1]).toBe(0);
}, 20000);
it("Player faster than opponent 150 vs 50", async() => {
await game.startBattle([
Species.BULBASAUR,
]);
game.scene.getParty()[0].stats[Stat.SPD] = 150;
game.scene.currentBattle.enemyParty[0].stats[Stat.SPD] = 50;
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
const movePosition = getMovePosition(game.scene, 0, Moves.TACKLE);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
await game.phaseInterceptor.run(EnemyCommandPhase);
await game.phaseInterceptor.whenAboutToRun(TurnStartPhase);
const phase = game.scene.getCurrentPhase() as TurnStartPhase;
const order = phase.getOrder();
expect(order[0]).toBe(0);
expect(order[1]).toBe(2);
}, 20000);
it("double - both opponents faster than player 50/50 vs 150/150", async() => {
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false);
vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
await game.startBattle([
Species.BULBASAUR,
Species.BLASTOISE,
]);
game.scene.getParty()[0].stats[Stat.SPD] = 50;
game.scene.getParty()[1].stats[Stat.SPD] = 50;
game.scene.currentBattle.enemyParty[0].stats[Stat.SPD] = 150;
game.scene.currentBattle.enemyParty[1].stats[Stat.SPD] = 150;
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
const movePosition = getMovePosition(game.scene, 0, Moves.TACKLE);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
game.onNextPrompt("SelectTargetPhase", Mode.TARGET_SELECT, () => {
const handler = game.scene.ui.getHandler() as TargetSelectUiHandler;
handler.processInput(Button.ACTION);
});
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
const movePosition = getMovePosition(game.scene, 0, Moves.TACKLE);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
game.onNextPrompt("SelectTargetPhase", Mode.TARGET_SELECT, () => {
const handler = game.scene.ui.getHandler() as TargetSelectUiHandler;
handler.processInput(Button.ACTION);
});
await game.phaseInterceptor.runFrom(CommandPhase).to(EnemyCommandPhase);
await game.phaseInterceptor.run(EnemyCommandPhase);
await game.phaseInterceptor.whenAboutToRun(TurnStartPhase);
const phase = game.scene.getCurrentPhase() as TurnStartPhase;
const order = phase.getOrder();
expect(order.indexOf(0)).toBeGreaterThan(order.indexOf(2));
expect(order.indexOf(0)).toBeGreaterThan(order.indexOf(3));
expect(order.indexOf(1)).toBeGreaterThan(order.indexOf(2));
expect(order.indexOf(1)).toBeGreaterThan(order.indexOf(3));
}, 20000);
it("double - speed tie except 1 - 100/100 vs 100/150", async() => {
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false);
vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
await game.startBattle([
Species.BULBASAUR,
Species.BLASTOISE,
]);
game.scene.getParty()[0].stats[Stat.SPD] = 100;
game.scene.getParty()[1].stats[Stat.SPD] = 100;
game.scene.currentBattle.enemyParty[0].stats[Stat.SPD] = 100;
game.scene.currentBattle.enemyParty[1].stats[Stat.SPD] = 150;
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
const movePosition = getMovePosition(game.scene, 0, Moves.TACKLE);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
game.onNextPrompt("SelectTargetPhase", Mode.TARGET_SELECT, () => {
const handler = game.scene.ui.getHandler() as TargetSelectUiHandler;
handler.processInput(Button.ACTION);
});
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
const movePosition = getMovePosition(game.scene, 0, Moves.TACKLE);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
game.onNextPrompt("SelectTargetPhase", Mode.TARGET_SELECT, () => {
const handler = game.scene.ui.getHandler() as TargetSelectUiHandler;
handler.processInput(Button.ACTION);
});
await game.phaseInterceptor.runFrom(CommandPhase).to(EnemyCommandPhase);
await game.phaseInterceptor.run(EnemyCommandPhase);
await game.phaseInterceptor.whenAboutToRun(TurnStartPhase);
const phase = game.scene.getCurrentPhase() as TurnStartPhase;
const order = phase.getOrder();
expect(order.indexOf(3)).toBeLessThan(order.indexOf(0));
expect(order.indexOf(3)).toBeLessThan(order.indexOf(1));
expect(order.indexOf(3)).toBeLessThan(order.indexOf(2));
}, 20000);
it("double - speed tie 100/150 vs 100/150", async() => {
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false);
vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
await game.startBattle([
Species.BULBASAUR,
Species.BLASTOISE,
]);
game.scene.getParty()[0].stats[Stat.SPD] = 100;
game.scene.getParty()[1].stats[Stat.SPD] = 150;
game.scene.currentBattle.enemyParty[0].stats[Stat.SPD] = 100;
game.scene.currentBattle.enemyParty[1].stats[Stat.SPD] = 150;
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
const movePosition = getMovePosition(game.scene, 0, Moves.TACKLE);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
game.onNextPrompt("SelectTargetPhase", Mode.TARGET_SELECT, () => {
const handler = game.scene.ui.getHandler() as TargetSelectUiHandler;
handler.processInput(Button.ACTION);
});
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
const movePosition = getMovePosition(game.scene, 0, Moves.TACKLE);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
game.onNextPrompt("SelectTargetPhase", Mode.TARGET_SELECT, () => {
const handler = game.scene.ui.getHandler() as TargetSelectUiHandler;
handler.processInput(Button.ACTION);
});
await game.phaseInterceptor.runFrom(CommandPhase).to(EnemyCommandPhase);
await game.phaseInterceptor.run(EnemyCommandPhase);
await game.phaseInterceptor.whenAboutToRun(TurnStartPhase);
const phase = game.scene.getCurrentPhase() as TurnStartPhase;
const order = phase.getOrder();
expect(order.indexOf(1)).toBeLessThan(order.indexOf(0));
expect(order.indexOf(1)).toBeLessThan(order.indexOf(2));
expect(order.indexOf(3)).toBeLessThan(order.indexOf(0));
expect(order.indexOf(3)).toBeLessThan(order.indexOf(2));
}, 20000);
});

View File

@ -0,0 +1,248 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import {generateStarter, getMovePosition, waitUntil,} from "#app/test/utils/gameManagerUtils";
import {Mode} from "#app/ui/ui";
import {GameModes} from "#app/game-mode";
import {Species} from "#app/data/enums/species";
import * as overrides from "../../overrides";
import {Command} from "#app/ui/command-ui-handler";
import {
BattleEndPhase,
BerryPhase,
CommandPhase,
DamagePhase,
EggLapsePhase,
EncounterPhase,
EnemyCommandPhase,
FaintPhase,
LoginPhase,
MessagePhase,
MoveEffectPhase,
MoveEndPhase,
MovePhase,
PostSummonPhase,
SelectGenderPhase,
SelectModifierPhase,
SelectStarterPhase,
StatChangePhase,
TitlePhase,
TurnEndPhase,
TurnInitPhase,
TurnStartPhase,
VictoryPhase,
} from "#app/phases";
import {Moves} from "#app/data/enums/moves";
import GameManager from "#app/test/utils/gameManager";
import Phaser from "phaser";
import {allSpecies} from "#app/data/pokemon-species";
import {PlayerGender} from "#app/data/enums/player-gender";
describe("Test Battle Phase", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
});
it("test phase interceptor with remove", async() => {
await game.phaseInterceptor.run(LoginPhase);
await game.phaseInterceptor.run(LoginPhase, () => {
return game.phaseInterceptor.log.includes("LoginPhase");
});
game.scene.gameData.gender = PlayerGender.MALE;
await game.phaseInterceptor.remove(SelectGenderPhase, () => game.isCurrentPhase(TitlePhase));
await game.phaseInterceptor.run(TitlePhase);
await waitUntil(() => game.scene.ui?.getMode() === Mode.TITLE);
expect(game.scene.ui?.getMode()).toBe(Mode.TITLE);
}, 100000);
it("test phase interceptor with prompt", async() => {
await game.phaseInterceptor.run(LoginPhase);
game.onNextPrompt("SelectGenderPhase", Mode.OPTION_SELECT, () => {
game.scene.gameData.gender = PlayerGender.MALE;
game.endPhase();
});
await game.phaseInterceptor.run(SelectGenderPhase);
await game.phaseInterceptor.run(TitlePhase);
await game.waitMode(Mode.TITLE);
expect(game.scene.ui?.getMode()).toBe(Mode.TITLE);
expect(game.scene.gameData.gender).toBe(PlayerGender.MALE);
}, 100000);
it("test phase interceptor with prompt with preparation for a future prompt", async() => {
await game.phaseInterceptor.run(LoginPhase);
game.onNextPrompt("SelectGenderPhase", Mode.OPTION_SELECT, () => {
game.scene.gameData.gender = PlayerGender.MALE;
game.endPhase();
});
game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => {
game.setMode(Mode.MESSAGE);
game.endPhase();
});
await game.phaseInterceptor.run(SelectGenderPhase);
await game.phaseInterceptor.run(TitlePhase);
await game.waitMode(Mode.TITLE);
expect(game.scene.ui?.getMode()).toBe(Mode.TITLE);
expect(game.scene.gameData.gender).toBe(PlayerGender.MALE);
}, 100000);
it("newGame one-liner", async() => {
await game.startBattle();
expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND);
expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name);
}, 100000);
it("do attack wave 3 - single battle - regular - OHKO", async() => {
vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MEWTWO);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA);
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(2000);
vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
await game.startBattle();
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
const movePosition = getMovePosition(game.scene, 0, Moves.TACKLE);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
await game.phaseInterceptor.run(EnemyCommandPhase);
await game.phaseInterceptor.run(TurnStartPhase);
await game.phaseInterceptor.run(MovePhase);
await game.phaseInterceptor.run(MessagePhase);
await game.phaseInterceptor.run(MoveEffectPhase);
await game.phaseInterceptor.run(DamagePhase);
await game.phaseInterceptor.run(MessagePhase, () => game.isCurrentPhase(FaintPhase));
await game.phaseInterceptor.run(FaintPhase);
await game.phaseInterceptor.run(MessagePhase);
await game.phaseInterceptor.run(VictoryPhase);
await game.phaseInterceptor.run(MoveEndPhase);
await game.phaseInterceptor.run(MovePhase);
await game.phaseInterceptor.run(BerryPhase);
await game.phaseInterceptor.run(TurnEndPhase);
await game.phaseInterceptor.run(BattleEndPhase);
await game.phaseInterceptor.run(EggLapsePhase);
await game.phaseInterceptor.run(SelectModifierPhase);
expect(game.scene.ui?.getMode()).toBe(Mode.MODIFIER_SELECT);
expect(game.scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);
}, 100000);
it("do attack wave 3 - single battle - regular - NO OHKO with opponent using non damage attack", async() => {
vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MEWTWO);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA);
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(5);
vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TAIL_WHIP]);
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
await game.startBattle();
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
const movePosition = getMovePosition(game.scene, 0, Moves.TACKLE);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
await game.phaseInterceptor.run(EnemyCommandPhase);
await game.phaseInterceptor.run(TurnStartPhase);
await game.phaseInterceptor.run(MovePhase);
await game.phaseInterceptor.run(MessagePhase);
await game.phaseInterceptor.run(MoveEffectPhase);
await game.phaseInterceptor.run(DamagePhase);
await game.phaseInterceptor.run(MessagePhase, () => game.isCurrentPhase(MoveEndPhase));
await game.phaseInterceptor.run(MoveEndPhase);
await game.phaseInterceptor.run(MovePhase);
await game.phaseInterceptor.run(MessagePhase, () => game.isCurrentPhase(MoveEffectPhase));
await game.phaseInterceptor.run(MoveEffectPhase);
game.scene.moveAnimations = null; // Mandatory to avoid the crash
await game.phaseInterceptor.run(StatChangePhase, () => game.isCurrentPhase(MessagePhase) || game.isCurrentPhase(MoveEndPhase) || game.isCurrentPhase(DamagePhase));
await game.phaseInterceptor.run(DamagePhase, () => game.isCurrentPhase(MessagePhase) || game.isCurrentPhase(MoveEndPhase));
await game.phaseInterceptor.run(MessagePhase, () => game.isCurrentPhase(MoveEndPhase));
await game.phaseInterceptor.run(MoveEndPhase);
await game.phaseInterceptor.run(BerryPhase);
await game.phaseInterceptor.run(MessagePhase, () => game.isCurrentPhase(TurnEndPhase));
await game.phaseInterceptor.run(TurnEndPhase);
await game.phaseInterceptor.run(TurnInitPhase);
await game.phaseInterceptor.run(CommandPhase);
await waitUntil(() => game.scene.ui?.getMode() === Mode.COMMAND);
expect(game.scene.ui?.getMode()).toBe(Mode.COMMAND);
expect(game.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name);
}, 100000);
it("load 100% data file", async() => {
await game.importData("src/test/utils/saves/everything.prsv");
const caughtCount = Object.keys(game.scene.gameData.dexData).filter((key) => {
const species = game.scene.gameData.dexData[key];
return species.caughtAttr !== 0n;
}).length;
expect(caughtCount).toBe(Object.keys(allSpecies).length);
}, 50000);
it("start battle with selected team", async() => {
await game.startBattle([
Species.CHARIZARD,
Species.CHANSEY,
Species.MEW
]);
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.CHARIZARD);
expect(game.scene.getParty()[1].species.speciesId).toBe(Species.CHANSEY);
expect(game.scene.getParty()[2].species.speciesId).toBe(Species.MEW);
}, 50000);
it("assert next phase", async() => {
await game.phaseInterceptor.run(LoginPhase);
game.onNextPrompt("SelectGenderPhase", Mode.OPTION_SELECT, () => {
game.scene.gameData.gender = PlayerGender.MALE;
game.endPhase();
}, () => game.isCurrentPhase(TitlePhase));
await game.phaseInterceptor.mustRun(SelectGenderPhase).catch((error) => expect(error).toBe(SelectGenderPhase));
await game.phaseInterceptor.mustRun(TitlePhase).catch((error) => expect(error).toBe(TitlePhase));
game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
const starters = generateStarter(game.scene);
const selectStarterPhase = new SelectStarterPhase(game.scene, GameModes.CLASSIC);
game.scene.pushPhase(new EncounterPhase(game.scene, false));
selectStarterPhase.initBattle(starters);
});
await game.phaseInterceptor.mustRun(EncounterPhase).catch((error) => expect(error).toBe(EncounterPhase));
await game.phaseInterceptor.mustRun(PostSummonPhase).catch((error) => expect(error).toBe(PostSummonPhase));
}, 50000);
it("test remove random battle seed int", async() => {
for (let i=0; i<10; i++) {
const rand = game.scene.randBattleSeedInt(15);
expect(rand).toBe(14);
}
});
});

33
src/test/eggs/egg.test.ts Normal file
View File

@ -0,0 +1,33 @@
import {beforeAll, describe, expect, it} from "vitest";
import BattleScene from "../../battle-scene";
import { getLegendaryGachaSpeciesForTimestamp } from "#app/data/egg.js";
import { Species } from "#app/data/enums/species.js";
import Phaser from "phaser";
describe("getLegendaryGachaSpeciesForTimestamp", () => {
beforeAll(() => {
new Phaser.Game({
type: Phaser.HEADLESS,
});
});
it("should return Arceus for the 10th of June", () => {
const scene = new BattleScene();
const timestamp = new Date(2024, 5, 10, 15, 0, 0, 0).getTime();
const expectedSpecies = Species.ARCEUS;
const result = getLegendaryGachaSpeciesForTimestamp(scene, timestamp);
expect(result).toBe(expectedSpecies);
});
it("should return Arceus for the 10th of July", () => {
const scene = new BattleScene();
const timestamp = new Date(2024, 6, 10, 15, 0, 0, 0).getTime();
const expectedSpecies = Species.ARCEUS;
const result = getLegendaryGachaSpeciesForTimestamp(scene, timestamp);
expect(result).toBe(expectedSpecies);
});
});

View File

@ -0,0 +1,105 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it} from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import pad_xbox360 from "#app/configs/inputs/pad_xbox360";
import cfg_keyboard_qwerty from "#app/configs/inputs/cfg_keyboard_qwerty";
import InputsHandler from "#app/test/utils/inputsHandler";
describe("Inputs", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
let originalDocument: Document;
beforeAll(() => {
originalDocument = window.document;
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
Object.defineProperty(window, "document", {
value: originalDocument,
configurable: true,
writable: true,
});
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.inputsHandler = new InputsHandler(game.scene);
});
it("Mobile - test touch holding for 1ms - 1 input", async () => {
await game.inputsHandler.pressTouch("dpadUp", 1);
expect(game.inputsHandler.log.length).toBe(1);
});
it("Mobile - test touch holding for 200ms - 1 input", async () => {
await game.inputsHandler.pressTouch("dpadUp", 200);
expect(game.inputsHandler.log.length).toBe(1);
});
it("Mobile - test touch holding for 300ms - 2 input", async () => {
await game.inputsHandler.pressTouch("dpadUp", 300);
expect(game.inputsHandler.log.length).toBe(2);
});
it("Mobile - test touch holding for 1000ms - 4 input", async () => {
await game.inputsHandler.pressTouch("dpadUp", 1000);
expect(game.inputsHandler.log.length).toBe(4);
});
it("keyboard - test input holding for 1ms - 1 input", async() => {
await game.inputsHandler.pressKeyboardKey(cfg_keyboard_qwerty.deviceMapping.KEY_ARROW_UP, 1);
expect(game.inputsHandler.log.length).toBe(1);
});
it("keyboard - test input holding for 200ms - 1 input", async() => {
await game.inputsHandler.pressKeyboardKey(cfg_keyboard_qwerty.deviceMapping.KEY_ARROW_UP, 200);
expect(game.inputsHandler.log.length).toBe(1);
});
it("keyboard - test input holding for 300ms - 2 input", async() => {
await game.inputsHandler.pressKeyboardKey(cfg_keyboard_qwerty.deviceMapping.KEY_ARROW_UP, 300);
expect(game.inputsHandler.log.length).toBe(2);
});
it("keyboard - test input holding for 1000ms - 4 input", async() => {
await game.inputsHandler.pressKeyboardKey(cfg_keyboard_qwerty.deviceMapping.KEY_ARROW_UP, 1000);
expect(game.inputsHandler.log.length).toBe(4);
});
it("keyboard - test input holding for 2000ms - 8 input", async() => {
await game.inputsHandler.pressKeyboardKey(cfg_keyboard_qwerty.deviceMapping.KEY_ARROW_UP, 2000);
expect(game.inputsHandler.log.length).toBe(8);
});
it("gamepad - test input holding for 1ms - 1 input", async() => {
await game.inputsHandler.pressGamepadButton(pad_xbox360.deviceMapping.RC_S, 1);
expect(game.inputsHandler.log.length).toBe(1);
});
it("gamepad - test input holding for 200ms - 1 input", async() => {
await game.inputsHandler.pressGamepadButton(pad_xbox360.deviceMapping.RC_S, 200);
expect(game.inputsHandler.log.length).toBe(1);
});
it("gamepad - test input holding for 300ms - 2 input", async() => {
await game.inputsHandler.pressGamepadButton(pad_xbox360.deviceMapping.RC_S, 300);
expect(game.inputsHandler.log.length).toBe(2);
});
it("gamepad - test input holding for 1000ms - 4 input", async() => {
await game.inputsHandler.pressGamepadButton(pad_xbox360.deviceMapping.RC_S, 1000);
expect(game.inputsHandler.log.length).toBe(4);
});
it("gamepad - test input holding for 2000ms - 8 input", async() => {
await game.inputsHandler.pressGamepadButton(pad_xbox360.deviceMapping.RC_S, 2000);
expect(game.inputsHandler.log.length).toBe(8);
});
});

View File

@ -0,0 +1,80 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import * as overrides from "#app/overrides";
import {Abilities} from "#app/data/enums/abilities";
import {Species} from "#app/data/enums/species";
import {
CommandPhase,
EnemyCommandPhase,
MessagePhase,
TurnEndPhase,
} from "#app/phases";
import {Mode} from "#app/ui/ui";
import {Moves} from "#app/data/enums/moves";
import {getMovePosition} from "#app/test/utils/gameManagerUtils";
import {Command} from "#app/ui/command-ui-handler";
import {StatusEffect} from "#app/data/status-effect";
describe("Items - Toxic orb", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
const moveToUse = Moves.GROWTH;
const oppMoveToUse = Moves.TACKLE;
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INSOMNIA);
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INSOMNIA);
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(2000);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]);
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([oppMoveToUse, oppMoveToUse, oppMoveToUse, oppMoveToUse]);
vi.spyOn(overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{
name: "TOXIC_ORB",
}]);
});
it("TOXIC ORB", async() => {
const moveToUse = Moves.GROWTH;
await game.startBattle([
Species.MIGHTYENA,
Species.MIGHTYENA,
]);
expect(game.scene.modifiers[0].type.id).toBe("TOXIC_ORB");
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
// Select Attack
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
// Select Move Growth
const movePosition = getMovePosition(game.scene, 0, moveToUse);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
// will run the 13 phase from enemyCommandPhase to TurnEndPhase
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase);
// Toxic orb should trigger here
await game.phaseInterceptor.run(MessagePhase);
const message = game.textInterceptor.getLatestMessage();
expect(message).toContain("was badly poisoned by Toxic Orb");
await game.phaseInterceptor.run(MessagePhase);
const message2 = game.textInterceptor.getLatestMessage();
expect(message2).toContain("is hurt");
expect(message2).toContain("by poison");
expect(game.scene.getParty()[0].status.effect).toBe(StatusEffect.TOXIC);
}, 20000);
});

View File

@ -0,0 +1,42 @@
import {afterEach, beforeAll, describe, expect, it} from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import {Species} from "#app/data/enums/species";
import i18next from "i18next";
import {initI18n} from "#app/plugins/i18n";
describe("Lokalization - french", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
initI18n();
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
it("test bulbasaur name english", async () => {
game = new GameManager(phaserGame);
await game.startBattle([
Species.BULBASAUR,
]);
expect(game.scene.getParty()[0].name).toBe("Bulbasaur");
}, 20000);
it("test bulbasaure name french", async () => {
const locale = "fr";
i18next.changeLanguage(locale);
localStorage.setItem("prLang", locale);
game = new GameManager(phaserGame);
await game.startBattle([
Species.BULBASAUR,
]);
expect(game.scene.getParty()[0].name).toBe("Bulbizarre");
}, 20000);
});

View File

@ -0,0 +1,69 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import * as overrides from "#app/overrides";
import {Abilities} from "#app/data/enums/abilities";
import {Species} from "#app/data/enums/species";
import {
CommandPhase,
EnemyCommandPhase,
TurnInitPhase,
} from "#app/phases";
import {Mode} from "#app/ui/ui";
import {Stat} from "#app/data/pokemon-stat";
import {Moves} from "#app/data/enums/moves";
import {getMovePosition} from "#app/test/utils/gameManagerUtils";
import {Command} from "#app/ui/command-ui-handler";
import {BattleStat} from "#app/data/battle-stat";
describe("Moves - Growth", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
const moveToUse = Moves.GROWTH;
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA);
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.MOXIE);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INSOMNIA);
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(2000);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]);
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]);
});
it("GROWTH", async() => {
const moveToUse = Moves.GROWTH;
await game.startBattle([
Species.MIGHTYENA,
Species.MIGHTYENA,
]);
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[Stat.SPATK]).toBe(0);
const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.SPATK]).toBe(0);
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
const movePosition = getMovePosition(game.scene, 0, moveToUse);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnInitPhase);
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.SPATK]).toBe(1);
}, 20000);
});

View File

@ -0,0 +1,85 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import * as overrides from "#app/overrides";
import {Species} from "#app/data/enums/species";
import {
CommandPhase,
EnemyCommandPhase, TurnEndPhase,
} from "#app/phases";
import {Mode} from "#app/ui/ui";
import {Moves} from "#app/data/enums/moves";
import {getMovePosition} from "#app/test/utils/gameManagerUtils";
import {Command} from "#app/ui/command-ui-handler";
import {Stat} from "#app/data/pokemon-stat";
describe("Moves - Tackle", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
const moveToUse = Moves.TACKLE;
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP);
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(1);
vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(97);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]);
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.GROWTH,Moves.GROWTH,Moves.GROWTH,Moves.GROWTH]);
vi.spyOn(overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true);
});
it("TACKLE against ghost", async() => {
const moveToUse = Moves.TACKLE;
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.GENGAR);
await game.startBattle([
Species.MIGHTYENA,
]);
const hpOpponent = game.scene.currentBattle.enemyParty[0].hp;
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
const movePosition = getMovePosition(game.scene, 0, moveToUse);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase);
const hpLost = hpOpponent - game.scene.currentBattle.enemyParty[0].hp;
expect(hpLost).toBe(0);
}, 20000);
it("TACKLE against not resistant", async() => {
const moveToUse = Moves.TACKLE;
await game.startBattle([
Species.MIGHTYENA,
]);
game.scene.currentBattle.enemyParty[0].stats[Stat.DEF] = 50;
game.scene.getParty()[0].stats[Stat.ATK] = 50;
const hpOpponent = game.scene.currentBattle.enemyParty[0].hp;
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
const movePosition = getMovePosition(game.scene, 0, moveToUse);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase);
const hpLost = hpOpponent - game.scene.currentBattle.enemyParty[0].hp;
expect(hpLost).toBeGreaterThan(0);
expect(hpLost).toBe(4);
}, 20000);
});

View File

@ -0,0 +1,66 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import * as overrides from "#app/overrides";
import {Abilities} from "#app/data/enums/abilities";
import {Species} from "#app/data/enums/species";
import {
CommandPhase,
EnemyCommandPhase,
TurnInitPhase,
} from "#app/phases";
import {Mode} from "#app/ui/ui";
import {Moves} from "#app/data/enums/moves";
import {getMovePosition} from "#app/test/utils/gameManagerUtils";
import {Command} from "#app/ui/command-ui-handler";
import {BattleStat} from "#app/data/battle-stat";
describe("Moves - Tail whip", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
const moveToUse = Moves.TAIL_WHIP;
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA);
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INSOMNIA);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INSOMNIA);
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(2000);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]);
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]);
});
it("TAIL_WHIP", async() => {
const moveToUse = Moves.TAIL_WHIP;
await game.startBattle([
Species.MIGHTYENA,
Species.MIGHTYENA,
]);
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.DEF]).toBe(0);
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
});
game.onNextPrompt("CommandPhase", Mode.FIGHT, () => {
const movePosition = getMovePosition(game.scene, 0, moveToUse);
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
});
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnInitPhase);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.DEF]).toBe(-1);
}, 20000);
});

View File

@ -1,5 +0,0 @@
import Phaser from "phaser";
export default new Phaser.Game({
type: Phaser.HEADLESS,
});

View File

@ -0,0 +1,51 @@
import BattleScene from "#app/battle-scene.js";
import { LoginPhase, TitlePhase, UnavailablePhase } from "#app/phases.js";
import { Mode } from "#app/ui/ui.js";
import {afterEach, beforeAll, beforeEach, describe, expect, it} from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
describe("Phases", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
let scene: BattleScene;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
scene = game.scene;
});
describe("LoginPhase", () => {
it("should start the login phase", async () => {
const loginPhase = new LoginPhase(scene);
loginPhase.start();
expect(scene.ui.getMode()).to.equal(Mode.MESSAGE);
});
});
describe("TitlePhase", () => {
it("should start the title phase", async () => {
const titlePhase = new TitlePhase(scene);
titlePhase.start();
expect(scene.ui.getMode()).to.equal(Mode.MESSAGE);
});
});
describe("UnavailablePhase", () => {
it("should start the unavailable phase", async () => {
const unavailablePhase = new UnavailablePhase(scene);
unavailablePhase.start();
expect(scene.ui.getMode()).to.equal(Mode.UNAVAILABLE);
});
});
});

View File

@ -1,57 +0,0 @@
import {describe, expect, it} from "vitest";
import {getPokemonSpecies} from "#app/data/pokemon-species";
import {PokemonMove} from "#app/field/pokemon";
import {Species} from "#app/data/enums/species";
import {Moves} from "#app/data/enums/moves";
import PokemonData from "#app/system/pokemon-data";
describe("some tests related to PokemonData and Species", () => {
it("should create a species", () => {
const species = getPokemonSpecies(Species.MEW);
expect(species).not.toBeNull();
});
it("should create a pokemon", () => {
const pokemon = new PokemonData({
species: Species.MEW,
level: 1,
});
expect(pokemon).not.toBeNull();
expect(pokemon.level).toEqual(1);
expect(pokemon.species).toEqual(Species.MEW);
});
it("should generate a moveset", () => {
const pokemon = new PokemonData({
species: Species.MEW,
level: 1,
});
expect(pokemon.moveset[0].moveId).toBe(Moves.TACKLE);
expect(pokemon.moveset[1].moveId).toBe(Moves.GROWL);
});
it("should create an ennemypokemon", () => {
const ennemyPokemon = new PokemonData({
species: Species.MEWTWO,
level: 100,
});
expect(ennemyPokemon).not.toBeNull();
expect(ennemyPokemon.level).toEqual(100);
expect(ennemyPokemon.species).toEqual(Species.MEWTWO);
});
it("should create an ennemypokemon with specified moveset", () => {
const ennemyPokemon = new PokemonData({
species: Species.MEWTWO,
level: 100,
moveset: [
new PokemonMove(Moves.ACID),
new PokemonMove(Moves.ACROBATICS),
new PokemonMove(Moves.FOCUS_ENERGY),
]
});
expect(ennemyPokemon.moveset[0].moveId).toBe(Moves.ACID);
expect(ennemyPokemon.moveset[1].moveId).toBe(Moves.ACROBATICS);
expect(ennemyPokemon.moveset[2].moveId).toBe(Moves.FOCUS_ENERGY);
});
});

View File

@ -5,8 +5,8 @@ import {
getKeyWithKeycode, getKeyWithKeycode,
getKeyWithSettingName, getKeyWithSettingName,
} from "#app/configs/inputs/configHandler"; } from "#app/configs/inputs/configHandler";
import {MenuManip} from "#app/test/helpers/menuManip"; import {MenuManip} from "#app/test/settingMenu/helpers/menuManip";
import {InGameManip} from "#app/test/helpers/inGameManip"; import {InGameManip} from "#app/test/settingMenu/helpers/inGameManip";
import {Device} from "#app/enums/devices"; import {Device} from "#app/enums/devices";
import {InterfaceConfig} from "#app/inputs-controller"; import {InterfaceConfig} from "#app/inputs-controller";
import cfg_keyboard_qwerty from "#app/configs/inputs/cfg_keyboard_qwerty"; import cfg_keyboard_qwerty from "#app/configs/inputs/cfg_keyboard_qwerty";

View File

@ -1,8 +1,8 @@
import {beforeAll, describe, expect, it} from "vitest"; import {beforeAll, describe, expect, it} from "vitest";
import _masterlist from "../../public/images/pokemon/variant/_masterlist.json"; import _masterlist from "../../../public/images/pokemon/variant/_masterlist.json";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import {getAppRootDir} from "#app/test/testUtils"; import {getAppRootDir} from "#app/test/sprites/spritesUtils";
const deepCopy = (data) => { const deepCopy = (data) => {
return JSON.parse(JSON.stringify(data)); return JSON.parse(JSON.stringify(data));

View File

@ -0,0 +1,612 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it} from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import {Species} from "#app/data/enums/species";
import {
EncounterPhase,
SelectStarterPhase,
TitlePhase,
} from "#app/phases";
import {Mode} from "#app/ui/ui";
import {GameModes} from "#app/game-mode";
import StarterSelectUiHandler from "#app/ui/starter-select-ui-handler";
import {Button} from "#app/enums/buttons";
import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler";
import SaveSlotSelectUiHandler from "#app/ui/save-slot-select-ui-handler";
import {OptionSelectItem} from "#app/ui/abstact-option-select-ui-handler";
import {Gender} from "#app/data/gender";
import {Nature} from "#app/data/nature";
import {Abilities} from "#app/data/enums/abilities";
import {allSpecies} from "#app/data/pokemon-species";
describe("UI - Starter select", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
});
it("Bulbasaur - shiny - variant 2 male", async() => {
await game.importData("src/test/utils/saves/everything.prsv");
const caughtCount = Object.keys(game.scene.gameData.dexData).filter((key) => {
const species = game.scene.gameData.dexData[key];
return species.caughtAttr !== 0n;
}).length;
expect(caughtCount).toBe(Object.keys(allSpecies).length);
await game.runToTitle();
game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
const currentPhase = game.scene.getCurrentPhase() as TitlePhase;
currentPhase.gameMode = GameModes.CLASSIC;
currentPhase.end();
});
await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase));
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.RIGHT);
handler.processInput(Button.ACTION);
});
let options: OptionSelectItem[];
let optionSelectUiHandler: OptionSelectUiHandler;
await new Promise<void>((resolve) => {
game.onNextPrompt("SelectStarterPhase", Mode.OPTION_SELECT, () => {
optionSelectUiHandler = game.scene.ui.getHandler() as OptionSelectUiHandler;
options = optionSelectUiHandler.getOptionsWithScroll();
resolve();
});
});
expect(options.some(option => option.label === "Add to Party")).toBe(true);
expect(options.some(option => option.label === "Toggle IVs")).toBe(true);
expect(options.some(option => option.label === "Manage Moves")).toBe(true);
expect(options.some(option => option.label === "Use Candies")).toBe(true);
expect(options.some(option => option.label === "Cancel")).toBe(true);
optionSelectUiHandler.processInput(Button.ACTION);
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.SUBMIT);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => {
const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler;
saveSlotSelectUiHandler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
expect(game.scene.getParty()[0].shiny).toBe(true);
expect(game.scene.getParty()[0].variant).toBe(2);
expect(game.scene.getParty()[0].gender).toBe(Gender.MALE);
}, 20000);
it("Bulbasaur - shiny - variant 2 female hardy overgrow", async() => {
await game.importData("src/test/utils/saves/everything.prsv");
await game.runToTitle();
game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
const currentPhase = game.scene.getCurrentPhase() as TitlePhase;
currentPhase.gameMode = GameModes.CLASSIC;
currentPhase.end();
});
await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase));
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.RIGHT);
handler.processInput(Button.CYCLE_GENDER);
handler.processInput(Button.ACTION);
});
let options: OptionSelectItem[];
let optionSelectUiHandler: OptionSelectUiHandler;
await new Promise<void>((resolve) => {
game.onNextPrompt("SelectStarterPhase", Mode.OPTION_SELECT, () => {
optionSelectUiHandler = game.scene.ui.getHandler() as OptionSelectUiHandler;
options = optionSelectUiHandler.getOptionsWithScroll();
resolve();
});
});
expect(options.some(option => option.label === "Add to Party")).toBe(true);
expect(options.some(option => option.label === "Toggle IVs")).toBe(true);
expect(options.some(option => option.label === "Manage Moves")).toBe(true);
expect(options.some(option => option.label === "Use Candies")).toBe(true);
expect(options.some(option => option.label === "Cancel")).toBe(true);
optionSelectUiHandler.processInput(Button.ACTION);
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.SUBMIT);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => {
const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler;
saveSlotSelectUiHandler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
expect(game.scene.getParty()[0].shiny).toBe(true);
expect(game.scene.getParty()[0].variant).toBe(2);
expect(game.scene.getParty()[0].nature).toBe(Nature.HARDY);
expect(game.scene.getParty()[0].getAbility().id).toBe(Abilities.OVERGROW);
}, 20000);
it("Bulbasaur - shiny - variant 2 female lonely cholorophyl", async() => {
await game.importData("src/test/utils/saves/everything.prsv");
await game.runToTitle();
game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
const currentPhase = game.scene.getCurrentPhase() as TitlePhase;
currentPhase.gameMode = GameModes.CLASSIC;
currentPhase.end();
});
await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase));
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.RIGHT);
handler.processInput(Button.CYCLE_GENDER);
handler.processInput(Button.CYCLE_NATURE);
handler.processInput(Button.CYCLE_ABILITY);
handler.processInput(Button.ACTION);
});
let options: OptionSelectItem[];
let optionSelectUiHandler: OptionSelectUiHandler;
await new Promise<void>((resolve) => {
game.onNextPrompt("SelectStarterPhase", Mode.OPTION_SELECT, () => {
optionSelectUiHandler = game.scene.ui.getHandler() as OptionSelectUiHandler;
options = optionSelectUiHandler.getOptionsWithScroll();
resolve();
});
});
expect(options.some(option => option.label === "Add to Party")).toBe(true);
expect(options.some(option => option.label === "Toggle IVs")).toBe(true);
expect(options.some(option => option.label === "Manage Moves")).toBe(true);
expect(options.some(option => option.label === "Use Candies")).toBe(true);
expect(options.some(option => option.label === "Cancel")).toBe(true);
optionSelectUiHandler.processInput(Button.ACTION);
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.SUBMIT);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => {
const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler;
saveSlotSelectUiHandler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
expect(game.scene.getParty()[0].shiny).toBe(true);
expect(game.scene.getParty()[0].variant).toBe(2);
expect(game.scene.getParty()[0].nature).toBe(Nature.LONELY);
expect(game.scene.getParty()[0].getAbility().id).toBe(Abilities.CHLOROPHYLL);
}, 20000);
it("Bulbasaur - shiny - variant 2 female", async() => {
await game.importData("src/test/utils/saves/everything.prsv");
await game.runToTitle();
game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
const currentPhase = game.scene.getCurrentPhase() as TitlePhase;
currentPhase.gameMode = GameModes.CLASSIC;
currentPhase.end();
});
await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase));
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.RIGHT);
handler.processInput(Button.CYCLE_GENDER);
handler.processInput(Button.ACTION);
});
let options: OptionSelectItem[];
let optionSelectUiHandler: OptionSelectUiHandler;
await new Promise<void>((resolve) => {
game.onNextPrompt("SelectStarterPhase", Mode.OPTION_SELECT, () => {
optionSelectUiHandler = game.scene.ui.getHandler() as OptionSelectUiHandler;
options = optionSelectUiHandler.getOptionsWithScroll();
resolve();
});
});
expect(options.some(option => option.label === "Add to Party")).toBe(true);
expect(options.some(option => option.label === "Toggle IVs")).toBe(true);
expect(options.some(option => option.label === "Manage Moves")).toBe(true);
expect(options.some(option => option.label === "Use Candies")).toBe(true);
expect(options.some(option => option.label === "Cancel")).toBe(true);
optionSelectUiHandler.processInput(Button.ACTION);
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.SUBMIT);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => {
const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler;
saveSlotSelectUiHandler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
expect(game.scene.getParty()[0].shiny).toBe(true);
expect(game.scene.getParty()[0].variant).toBe(2);
expect(game.scene.getParty()[0].gender).toBe(Gender.FEMALE);
}, 20000);
it("Bulbasaur - not shiny", async() => {
await game.importData("src/test/utils/saves/everything.prsv");
await game.runToTitle();
game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
const currentPhase = game.scene.getCurrentPhase() as TitlePhase;
currentPhase.gameMode = GameModes.CLASSIC;
currentPhase.end();
});
await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase));
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.RIGHT);
handler.processInput(Button.CYCLE_SHINY);
handler.processInput(Button.ACTION);
});
let options: OptionSelectItem[];
let optionSelectUiHandler: OptionSelectUiHandler;
await new Promise<void>((resolve) => {
game.onNextPrompt("SelectStarterPhase", Mode.OPTION_SELECT, () => {
optionSelectUiHandler = game.scene.ui.getHandler() as OptionSelectUiHandler;
options = optionSelectUiHandler.getOptionsWithScroll();
resolve();
});
});
expect(options.some(option => option.label === "Add to Party")).toBe(true);
expect(options.some(option => option.label === "Toggle IVs")).toBe(true);
expect(options.some(option => option.label === "Manage Moves")).toBe(true);
expect(options.some(option => option.label === "Use Candies")).toBe(true);
expect(options.some(option => option.label === "Cancel")).toBe(true);
optionSelectUiHandler.processInput(Button.ACTION);
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.SUBMIT);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => {
const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler;
saveSlotSelectUiHandler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
expect(game.scene.getParty()[0].shiny).toBe(false);
expect(game.scene.getParty()[0].variant).toBe(0);
}, 20000);
it("Bulbasaur - shiny - variant 0", async() => {
await game.importData("src/test/utils/saves/everything.prsv");
await game.runToTitle();
game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
const currentPhase = game.scene.getCurrentPhase() as TitlePhase;
currentPhase.gameMode = GameModes.CLASSIC;
currentPhase.end();
});
await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase));
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.RIGHT);
handler.processInput(Button.V);
handler.processInput(Button.ACTION);
});
let options: OptionSelectItem[];
let optionSelectUiHandler: OptionSelectUiHandler;
await new Promise<void>((resolve) => {
game.onNextPrompt("SelectStarterPhase", Mode.OPTION_SELECT, () => {
optionSelectUiHandler = game.scene.ui.getHandler() as OptionSelectUiHandler;
options = optionSelectUiHandler.getOptionsWithScroll();
resolve();
});
});
expect(options.some(option => option.label === "Add to Party")).toBe(true);
expect(options.some(option => option.label === "Toggle IVs")).toBe(true);
expect(options.some(option => option.label === "Manage Moves")).toBe(true);
expect(options.some(option => option.label === "Use Candies")).toBe(true);
expect(options.some(option => option.label === "Cancel")).toBe(true);
optionSelectUiHandler.processInput(Button.ACTION);
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.SUBMIT);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => {
const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler;
saveSlotSelectUiHandler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
expect(game.scene.getParty()[0].shiny).toBe(true);
expect(game.scene.getParty()[0].variant).toBe(0);
}, 20000);
it("Bulbasaur - shiny - variant 1", async() => {
await game.importData("src/test/utils/saves/everything.prsv");
await game.runToTitle();
game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
const currentPhase = game.scene.getCurrentPhase() as TitlePhase;
currentPhase.gameMode = GameModes.CLASSIC;
currentPhase.end();
});
await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase));
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.RIGHT);
handler.processInput(Button.V);
handler.processInput(Button.V);
handler.processInput(Button.ACTION);
});
let options: OptionSelectItem[];
let optionSelectUiHandler: OptionSelectUiHandler;
await new Promise<void>((resolve) => {
game.onNextPrompt("SelectStarterPhase", Mode.OPTION_SELECT, () => {
optionSelectUiHandler = game.scene.ui.getHandler() as OptionSelectUiHandler;
options = optionSelectUiHandler.getOptionsWithScroll();
resolve();
});
});
expect(options.some(option => option.label === "Add to Party")).toBe(true);
expect(options.some(option => option.label === "Toggle IVs")).toBe(true);
expect(options.some(option => option.label === "Manage Moves")).toBe(true);
expect(options.some(option => option.label === "Use Candies")).toBe(true);
expect(options.some(option => option.label === "Cancel")).toBe(true);
optionSelectUiHandler.processInput(Button.ACTION);
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.SUBMIT);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => {
const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler;
saveSlotSelectUiHandler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
expect(game.scene.getParty()[0].shiny).toBe(true);
expect(game.scene.getParty()[0].variant).toBe(1);
}, 20000);
it("Bulbasaur - shiny - variant 1", async() => {
await game.importData("src/test/utils/saves/everything.prsv");
await game.runToTitle();
game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
const currentPhase = game.scene.getCurrentPhase() as TitlePhase;
currentPhase.gameMode = GameModes.CLASSIC;
currentPhase.end();
});
await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase));
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.RIGHT);
handler.processInput(Button.V);
handler.processInput(Button.V);
handler.processInput(Button.V);
handler.processInput(Button.ACTION);
});
let options: OptionSelectItem[];
let optionSelectUiHandler: OptionSelectUiHandler;
await new Promise<void>((resolve) => {
game.onNextPrompt("SelectStarterPhase", Mode.OPTION_SELECT, () => {
optionSelectUiHandler = game.scene.ui.getHandler() as OptionSelectUiHandler;
options = optionSelectUiHandler.getOptionsWithScroll();
resolve();
});
});
expect(options.some(option => option.label === "Add to Party")).toBe(true);
expect(options.some(option => option.label === "Toggle IVs")).toBe(true);
expect(options.some(option => option.label === "Manage Moves")).toBe(true);
expect(options.some(option => option.label === "Use Candies")).toBe(true);
expect(options.some(option => option.label === "Cancel")).toBe(true);
optionSelectUiHandler.processInput(Button.ACTION);
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.SUBMIT);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => {
const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler;
saveSlotSelectUiHandler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
expect(game.scene.getParty()[0].shiny).toBe(true);
expect(game.scene.getParty()[0].variant).toBe(2);
}, 20000);
it("Check if first pokemon in party is caterpie from gen 1 and 1rd row, 3rd column ", async() => {
await game.importData("src/test/utils/saves/everything.prsv");
await game.runToTitle();
game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
const currentPhase = game.scene.getCurrentPhase() as TitlePhase;
currentPhase.gameMode = GameModes.CLASSIC;
currentPhase.end();
});
await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase));
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.RIGHT);
handler.processInput(Button.RIGHT);
handler.processInput(Button.RIGHT);
handler.processInput(Button.RIGHT);
handler.processInput(Button.ACTION);
});
let options: OptionSelectItem[];
let optionSelectUiHandler: OptionSelectUiHandler;
await new Promise<void>((resolve) => {
game.onNextPrompt("SelectStarterPhase", Mode.OPTION_SELECT, () => {
optionSelectUiHandler = game.scene.ui.getHandler() as OptionSelectUiHandler;
options = optionSelectUiHandler.getOptionsWithScroll();
resolve();
});
});
expect(options.some(option => option.label === "Add to Party")).toBe(true);
expect(options.some(option => option.label === "Toggle IVs")).toBe(true);
expect(options.some(option => option.label === "Manage Moves")).toBe(true);
expect(options.some(option => option.label === "Use Candies")).toBe(true);
expect(options.some(option => option.label === "Cancel")).toBe(true);
optionSelectUiHandler.processInput(Button.ACTION);
let starterSelectUiHandler: StarterSelectUiHandler;
await new Promise<void>((resolve) => {
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
starterSelectUiHandler = game.scene.ui.getHandler() as StarterSelectUiHandler;
starterSelectUiHandler.processInput(Button.SUBMIT);
resolve();
});
});
expect(starterSelectUiHandler.starterGens[0]).toBe(0);
expect(starterSelectUiHandler.starterCursors[0]).toBe(3);
expect(starterSelectUiHandler.cursorObj.x).toBe(132 + 4 * 18);
expect(starterSelectUiHandler.cursorObj.y).toBe(10);
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => {
const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler;
saveSlotSelectUiHandler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.CATERPIE);
}, 20000);
it("Check if first pokemon in party is nidoran_m from gen 1 and 2nd row, 4th column (cursor (9+4)-1) ", async() => {
await game.importData("src/test/utils/saves/everything.prsv");
await game.runToTitle();
game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
const currentPhase = game.scene.getCurrentPhase() as TitlePhase;
currentPhase.gameMode = GameModes.CLASSIC;
currentPhase.end();
});
await game.phaseInterceptor.mustRun(SelectStarterPhase).catch((error) => expect(error).toBe(SelectStarterPhase));
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.RIGHT);
handler.processInput(Button.RIGHT);
handler.processInput(Button.RIGHT);
handler.processInput(Button.RIGHT);
handler.processInput(Button.DOWN);
handler.processInput(Button.ACTION);
});
let options: OptionSelectItem[];
let optionSelectUiHandler: OptionSelectUiHandler;
await new Promise<void>((resolve) => {
game.onNextPrompt("SelectStarterPhase", Mode.OPTION_SELECT, () => {
optionSelectUiHandler = game.scene.ui.getHandler() as OptionSelectUiHandler;
options = optionSelectUiHandler.getOptionsWithScroll();
resolve();
});
});
expect(options.some(option => option.label === "Add to Party")).toBe(true);
expect(options.some(option => option.label === "Toggle IVs")).toBe(true);
expect(options.some(option => option.label === "Manage Moves")).toBe(true);
expect(options.some(option => option.label === "Use Candies")).toBe(true);
expect(options.some(option => option.label === "Cancel")).toBe(true);
optionSelectUiHandler.processInput(Button.ACTION);
let starterSelectUiHandler: StarterSelectUiHandler;
await new Promise<void>((resolve) => {
game.onNextPrompt("SelectStarterPhase", Mode.STARTER_SELECT, () => {
starterSelectUiHandler = game.scene.ui.getHandler() as StarterSelectUiHandler;
starterSelectUiHandler.processInput(Button.SUBMIT);
resolve();
});
});
expect(starterSelectUiHandler.starterGens[0]).toBe(0);
expect(starterSelectUiHandler.starterCursors[0]).toBe(12);
expect(starterSelectUiHandler.cursorObj.x).toBe(132 + 4 * 18);
expect(starterSelectUiHandler.cursorObj.y).toBe(28);
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.SAVE_SLOT, () => {
const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler;
saveSlotSelectUiHandler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectStarterPhase", Mode.CONFIRM, () => {
const handler = game.scene.ui.getHandler() as StarterSelectUiHandler;
handler.processInput(Button.ACTION);
});
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.NIDORAN_M);
}, 20000);
});

View File

@ -0,0 +1,16 @@
export default class TextInterceptor {
private scene;
private logs = [];
constructor(scene) {
this.scene = scene;
scene.messageWrapper = this;
}
showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer): void {
this.logs.push(text);
}
getLatestMessage(): string {
return this.logs[this.logs.length - 1];
}
}

View File

@ -0,0 +1,50 @@
<!DOCTYPE html><body>
<div id="touchControls">
<div id="dpad" class="unselectable">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72">
<path id="dpadUp" data-key="UP" d="M48,5.8C48,2.5,45.4,0,42,0H29.9C26.6,0,24,2.4,24,5.8V24h24V5.8z" />
<path id="dpadRight" data-key="RIGHT" d="M66.2,24H48v24h18.2c3.3,0,5.8-2.7,5.8-6V29.9C72,26.5,69.5,24,66.2,24z" />
<path id="dpadDown" data-key="DOWN" d="M24,66.3c0,3.3,2.6,5.7,5.9,5.7H42c3.3,0,6-2.4,6-5.7V48H24V66.3z" />
<path id="dpadLeft" data-key="LEFT" d="M5.7,24C2.4,24,0,26.5,0,29.9V42c0,3.3,2.3,6,5.7,6H24V24H5.7z" />
<rect id="dpadCenter" x="24" y="24" width="24" height="24" />
</svg>
</div>
<div id="apad" class="unselectable">
<div id="apadAction" class="apadCircBtn apadBtn" data-key="ACTION">
<text id="apadLabelAction" class="apadLabel">A</text>
</div>
<div id="apadCancel" class="apadCircBtn apadBtn" data-key="CANCEL">
<text id="apadLabelCancel" class="apadLabel">B</text>
</div>
<div class="apadBtnContainer apadRectBtnContainer">
<div id="apadCycleShiny" class="apadSqBtn apadBtn" data-key="CYCLE_SHINY">
<text class="apadLabel apadLabelSmall">R</text>
</div>
<div id="apadCycleVariant" class="apadSqBtn apadBtn" data-key="CYCLE_VARIANT">
<text class="apadLabel apadLabelSmall">V</text>
</div>
<div id="apadStats" class="apadRectBtn apadBtn" data-key="STATS">
<text class="apadLabel apadLabelSmall">C</text>
</div>
<div id="apadMenu" class="apadRectBtn apadBtn" data-key="MENU">
<text class="apadLabel apadLabelSmall">Menu</text>
</div>
</div>
<div class="apadBtnContainer apadSqBtnContainer">
<div id="apadCycleForm" class="apadSqBtn apadBtn" data-key="CYCLE_FORM">
<text class="apadLabel apadLabelSmall">F</text>
</div>
<div id="apadCycleGender" class="apadSqBtn apadBtn" data-key="CYCLE_GENDER">
<text class="apadLabel apadLabelSmall">G</text>
</div>
<div id="apadCycleAbility" class="apadSqBtn apadBtn" data-key="CYCLE_ABILITY">
<text class="apadLabel apadLabelSmall">E</text>
</div>
<div id="apadCycleNature" class="apadSqBtn apadBtn" data-key="CYCLE_NATURE">
<text class="apadLabel apadLabelSmall">N</text>
</div>
</div>
</div>
</div>
</body>

View File

@ -0,0 +1,223 @@
import GameWrapper from "#app/test/utils/gameWrapper";
import {Mode} from "#app/ui/ui";
import {generateStarter, waitUntil} from "#app/test/utils/gameManagerUtils";
import {
CheckSwitchPhase,
CommandPhase,
EncounterPhase,
LoginPhase,
PostSummonPhase,
SelectGenderPhase,
SelectStarterPhase,
SummonPhase,
TitlePhase,
ToggleDoublePositionPhase,
} from "#app/phases";
import BattleScene from "#app/battle-scene.js";
import PhaseInterceptor from "#app/test/utils/phaseInterceptor";
import TextInterceptor from "#app/test/utils/TextInterceptor";
import {expect} from "vitest";
import {GameModes} from "#app/game-mode";
import fs from "fs";
import { AES, enc } from "crypto-js";
import {updateUserInfo} from "#app/account";
import {Species} from "#app/data/enums/species";
import {PlayerGender} from "#app/data/enums/player-gender";
import {GameDataType} from "#app/data/enums/game-data-type";
import InputsHandler from "#app/test/utils/inputsHandler";
import {ExpNotification} from "#app/enums/exp-notification";
/**
* Class to manage the game state and transitions between phases.
*/
export default class GameManager {
public gameWrapper: GameWrapper;
public scene: BattleScene;
public phaseInterceptor: PhaseInterceptor;
public textInterceptor: TextInterceptor;
public inputsHandler: InputsHandler;
/**
* Creates an instance of GameManager.
* @param phaserGame - The Phaser game instance.
* @param bypassLogin - Whether to bypass the login phase.
*/
constructor(phaserGame: Phaser.Game, bypassLogin: boolean = true) {
BattleScene.prototype.randBattleSeedInt = (arg) => arg-1;
this.gameWrapper = new GameWrapper(phaserGame, bypassLogin);
this.scene = new BattleScene();
this.phaseInterceptor = new PhaseInterceptor(this.scene);
this.textInterceptor = new TextInterceptor(this.scene);
this.gameWrapper.setScene(this.scene);
}
/**
* Sets the game mode.
* @param mode - The mode to set.
*/
setMode(mode: Mode) {
this.scene.ui?.setMode(mode);
}
/**
* Waits until the specified mode is set.
* @param mode - The mode to wait for.
* @returns A promise that resolves when the mode is set.
*/
waitMode(mode: Mode): Promise<void> {
return new Promise(async (resolve) => {
await waitUntil(() => this.scene.ui?.getMode() === mode);
return resolve();
});
}
/**
* Ends the current phase.
*/
endPhase() {
this.scene.getCurrentPhase().end();
}
/**
* Adds an action to be executed on the next prompt.
* @param phaseTarget - The target phase.
* @param mode - The mode to wait for.
* @param callback - The callback to execute.
* @param expireFn - Optional function to determine if the prompt has expired.
*/
onNextPrompt(phaseTarget: string, mode: Mode, callback: () => void, expireFn?: () => void) {
this.phaseInterceptor.addToNextPrompt(phaseTarget, mode, callback, expireFn);
}
/**
* Runs the game to the title phase.
* @returns A promise that resolves when the title phase is reached.
*/
runToTitle(): Promise<void> {
return new Promise(async(resolve) => {
await this.phaseInterceptor.run(LoginPhase);
this.onNextPrompt("SelectGenderPhase", Mode.OPTION_SELECT, () => {
this.scene.gameData.gender = PlayerGender.MALE;
this.endPhase();
}, () => this.isCurrentPhase(TitlePhase));
await this.phaseInterceptor.run(SelectGenderPhase, () => this.isCurrentPhase(TitlePhase));
await this.phaseInterceptor.run(TitlePhase);
this.scene.gameSpeed = 5;
this.scene.moveAnimations = false;
this.scene.showLevelUpStats = false;
this.scene.expGainsSpeed = 3;
this.scene.expParty = ExpNotification.SKIP;
this.scene.hpBarSpeed = 3;
resolve();
});
}
/**
* Runs the game to the summon phase.
* @param species - Optional array of species to summon.
* @returns A promise that resolves when the summon phase is reached.
*/
runToSummon(species?: Species[]): Promise<void> {
return new Promise(async(resolve) => {
await this.runToTitle();
this.onNextPrompt("TitlePhase", Mode.TITLE, () => {
const starters = generateStarter(this.scene, species);
const selectStarterPhase = new SelectStarterPhase(this.scene, GameModes.CLASSIC);
this.scene.pushPhase(new EncounterPhase(this.scene, false));
selectStarterPhase.initBattle(starters);
});
await this.phaseInterceptor.run(EncounterPhase);
resolve();
});
}
/**
* Starts a battle.
* @param species - Optional array of species to start the battle with.
* @returns A promise that resolves when the battle is started.
*/
startBattle(species?: Species[]): Promise<void> {
return new Promise(async(resolve) => {
await this.runToSummon(species);
await this.phaseInterceptor.runFrom(PostSummonPhase).to(ToggleDoublePositionPhase);
await this.phaseInterceptor.run(SummonPhase, () => this.isCurrentPhase(CheckSwitchPhase) || this.isCurrentPhase(PostSummonPhase));
this.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => {
this.setMode(Mode.MESSAGE);
this.endPhase();
}, () => this.isCurrentPhase(PostSummonPhase));
this.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => {
this.setMode(Mode.MESSAGE);
this.endPhase();
}, () => this.isCurrentPhase(PostSummonPhase));
await this.phaseInterceptor.run(CheckSwitchPhase, () => this.isCurrentPhase(PostSummonPhase));
await this.phaseInterceptor.run(CheckSwitchPhase, () => this.isCurrentPhase(PostSummonPhase));
await this.phaseInterceptor.runFrom(PostSummonPhase).to(CommandPhase);
await waitUntil(() => this.scene.ui?.getMode() === Mode.COMMAND);
console.log("==================[New Turn]==================");
expect(this.scene.ui?.getMode()).toBe(Mode.COMMAND);
expect(this.scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name);
return resolve();
});
}
/**
* Checks if the player has won the battle.
* @returns True if the player has won, otherwise false.
*/
isVictory() {
return this.scene.currentBattle.enemyParty.every(pokemon => pokemon.isFainted());
}
/**
* Checks if the current phase matches the target phase.
* @param phaseTarget - The target phase.
* @returns True if the current phase matches the target phase, otherwise false.
*/
isCurrentPhase(phaseTarget) {
const targetName = typeof phaseTarget === "string" ? phaseTarget : phaseTarget.name;
return this.scene.getCurrentPhase().constructor.name === targetName;
}
/**
* Checks if the current mode matches the target mode.
* @param mode - The target mode.
* @returns True if the current mode matches the target mode, otherwise false.
*/
isCurrentMode(mode: Mode) {
return this.scene.ui?.getMode() === mode;
}
/**
* Exports the save data to import it in a test game.
* @returns A promise that resolves with the exported save data.
*/
exportSaveToTest(): Promise<string> {
return new Promise(async (resolve) => {
await this.scene.gameData.saveAll(this.scene, true, true, true, true);
this.scene.reset(true);
await waitUntil(() => this.scene.ui?.getMode() === Mode.TITLE);
await this.scene.gameData.tryExportData(GameDataType.SESSION, 0);
await waitUntil(() => localStorage.hasOwnProperty("toExport"));
return resolve(localStorage.getItem("toExport"));
});
}
/**
* Imports game data from a file.
* @param path - The path to the data file.
* @returns A promise that resolves with a tuple containing a boolean indicating success and an integer status code.
*/
async importData(path): Promise<[boolean, integer]> {
const saveKey = "x0i2O7WRiANTqPmZ";
const dataRaw = fs.readFileSync(path, {encoding: "utf8", flag: "r"});
let dataStr = AES.decrypt(dataRaw, saveKey).toString(enc.Utf8);
dataStr = this.scene.gameData.convertSystemDataStr(dataStr);
const systemData = this.scene.gameData.parseSystemData(dataStr);
const valid = !!systemData.dexData && !!systemData.timestamp;
if (valid) {
await updateUserInfo();
await this.scene.gameData.initSystem(dataStr);
}
return updateUserInfo();
}
}

View File

@ -0,0 +1,87 @@
// Function to convert Blob to string
import {getDailyRunStarters} from "#app/data/daily-run";
import {Gender} from "#app/data/gender";
import {Species} from "#app/data/enums/species";
import {Starter} from "#app/ui/starter-select-ui-handler";
import {GameModes, gameModes} from "#app/game-mode";
import {getPokemonSpecies, getPokemonSpeciesForm} from "#app/data/pokemon-species";
import {PlayerPokemon} from "#app/field/pokemon";
export function blobToString(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
resolve(reader.result);
};
reader.onerror = () => {
reject(new Error("Error reading Blob as string"));
};
reader.readAsText(blob);
});
}
export function holdOn(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function generateStarter(scene, species?: Species[]) {
const seed = "test";
const starters = getTestRunStarters(scene, seed, species);
const startingLevel = scene.gameMode.getStartingLevel();
for (const starter of starters) {
const starterProps = scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr);
const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0));
const starterGender = starter.species.malePercent !== null
? !starterProps.female ? Gender.MALE : Gender.FEMALE
: Gender.GENDERLESS;
const starterPokemon = scene.addPlayerPokemon(starter.species, startingLevel, starter.abilityIndex, starterFormIndex, starterGender, starterProps.shiny, starterProps.variant, undefined, starter.nature);
starter.moveset = starterPokemon.moveset;
}
return starters;
}
function getTestRunStarters(scene, seed, species) {
if (!species) {
return getDailyRunStarters(scene, seed);
}
const starters: Starter[] = [];
const startingLevel = gameModes[GameModes.CLASSIC].getStartingLevel();
for (const specie of species) {
const starterSpeciesForm = getPokemonSpeciesForm(specie, 0);
const starterSpecies = getPokemonSpecies(starterSpeciesForm.speciesId);
const pokemon = new PlayerPokemon(scene, starterSpecies, startingLevel, undefined, 0, undefined, undefined, undefined, undefined, undefined, undefined);
const starter: Starter = {
species: starterSpecies,
dexAttr: pokemon.getDexAttr(),
abilityIndex: pokemon.abilityIndex,
passive: false,
nature: pokemon.getNature(),
pokerus: pokemon.pokerus
};
starters.push(starter);
}
return starters;
}
export function waitUntil(truth) {
return new Promise(resolve => {
const interval = setInterval(() => {
if (truth()) {
clearInterval(interval);
resolve(true);
}
}, 1000);
});
}
export function getMovePosition(scene, pokemonIndex, moveIndex) {
const playerPokemon = scene.getPlayerField()[pokemonIndex];
const moveSet = playerPokemon.getMoveset();
const index = moveSet.findIndex((move) => move.moveId === moveIndex);
return index;
}

View File

@ -0,0 +1,252 @@
/* eslint-disable */
// @ts-nocheck
import * as main from "#app/main";
import fs from "fs";
import InputManager = Phaser.Input.InputManager;
import KeyboardManager = Phaser.Input.Keyboard.KeyboardManager;
import KeyboardPlugin = Phaser.Input.Keyboard.KeyboardPlugin;
import GamepadPlugin = Phaser.Input.Gamepad.GamepadPlugin;
import EventEmitter = Phaser.Events.EventEmitter;
import UpdateList = Phaser.GameObjects.UpdateList;
import MockGraphics from "#app/test/utils/mocks/mocksContainer/mockGraphics";
import MockTextureManager from "#app/test/utils/mocks/mockTextureManager";
import Phaser from "phaser";
import {blobToString} from "#app/test/utils/gameManagerUtils";
import {vi} from "vitest";
import mockLocalStorage from "#app/test/utils/mocks/mockLocalStorage";
import mockConsoleLog from "#app/test/utils/mocks/mockConsoleLog";
import MockLoader from "#app/test/utils/mocks/mockLoader";
import {MockFetch} from "#app/test/utils/mocks/mockFetch";
import * as Utils from "#app/utils";
import InputText from "phaser3-rex-plugins/plugins/inputtext";
import {MockClock} from "#app/test/utils/mocks/mockClock";
import BattleScene from "#app/battle-scene.js";
import {MoveAnim} from "#app/data/battle-anims";
import Pokemon from "#app/field/pokemon";
import * as battleScene from "#app/battle-scene";
Object.defineProperty(window, "localStorage", {
value: mockLocalStorage(),
});
Object.defineProperty(window, "console", {
value: mockConsoleLog(false),
});
InputText.prototype.setElement = () => null;
InputText.prototype.resize = () => null;
window.URL.createObjectURL = (blob: Blob) => {
blobToString(blob).then((data: string) => {
localStorage.setItem("toExport", data);
})
return null;
};
navigator.getGamepads = vi.fn().mockReturnValue([]);
global.fetch = vi.fn(MockFetch);
Utils.setCookie(Utils.sessionIdKey, 'fake_token');
window.matchMedia = () => ({
matches: false,
});
/**
* Sets this object's position relative to another object with a given offset
* @param guideObject {@linkcode Phaser.GameObjects.GameObject} to base the position off of
* @param x The relative x position
* @param y The relative y position
*/
const setPositionRelative = function (guideObject: any, x: number, y: number) {
const offsetX = guideObject.width * (-0.5 + (0.5 - guideObject.originX));
const offsetY = guideObject.height * (-0.5 + (0.5 - guideObject.originY));
this.setPosition(guideObject.x + offsetX + x, guideObject.y + offsetY + y);
};
Phaser.GameObjects.Container.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Sprite.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Image.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.NineSlice.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Text.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Rectangle.prototype.setPositionRelative = setPositionRelative;
export default class GameWrapper {
public game: Phaser.Game;
public scene: BattleScene;
constructor(phaserGame: Phaser.Game, bypassLogin: boolean) {
Phaser.Math.RND.sow([ 'test' ]);
vi.spyOn(Utils, "apiFetch", "get").mockReturnValue(fetch);
if (bypassLogin) {
vi.spyOn(battleScene, "bypassLogin", "get").mockReturnValue(true);
}
this.game = phaserGame;
MoveAnim.prototype.getAnim = () => ({
frames: {},
});
Pokemon.prototype.enableMask = () => null;
localStorage.clear();
}
setScene(scene: BattleScene) {
this.scene = scene;
this.injectMandatory();
this.scene.preload && this.scene.preload();
this.scene.create();
}
injectMandatory() {
this.game.config = {
seed: ["test"],
}
this.scene.game = this.game;
this.game.renderer = {
maxTextures: -1,
gl: {},
deleteTexture: () => null,
canvasToTexture: () => ({}),
createCanvasTexture: () => ({}),
pipelines: {
add: () => null,
},
};
this.scene.renderer = this.game.renderer;
this.scene.children = {
removeAll: () => null,
};
this.scene.sound = {
play: () => null,
pause: () => null,
setRate: () => null,
add: () => this.scene.sound,
get: () => this.scene.sound,
getAllPlaying: () => [],
manager: {
game: this.game,
},
setVolume: () => null,
stopByKey: () => null,
on: (evt, callback) => callback(),
key: "",
};
this.scene.tweens = {
add: (data) => {
if (data.onComplete) {
data.onComplete();
}
},
getTweensOf: () => ([]),
killTweensOf: () => ([]),
chain: () => null,
addCounter: (data) => {
if (data.onComplete) {
data.onComplete();
}
},
};
this.scene.anims = this.game.anims;
this.scene.cache = this.game.cache;
this.scene.plugins = this.game.plugins;
this.scene.registry = this.game.registry;
this.scene.scale = this.game.scale;
this.scene.textures = this.game.textures;
this.scene.events = this.game.events;
this.scene.manager = new InputManager(this.game, {});
this.scene.manager.keyboard = new KeyboardManager(this.scene);
this.scene.pluginEvents = new EventEmitter();
this.scene.domContainer = {} as HTMLDivElement;
this.scene.spritePipeline = {};
this.scene.fieldSpritePipeline = {};
this.scene.load = new MockLoader(this.scene);
this.scene.sys = {
queueDepthSort: () => null,
anims: this.game.anims,
game: this.game,
textures: {
addCanvas: () => ({
get: () => ({ // this.frame in Text.js
source: {},
setSize: () => null,
glTexture: () => ({
spectorMetadata: {},
}),
}),
})
},
cache: this.scene.load.cacheManager,
scale: this.game.scale,
// _scene.sys.scale = new ScaleManager(_scene);
// events: {
// on: () => null,
// },
events: new EventEmitter(),
settings: {
loader: {
key: 'battle',
}
},
input: this.game.input,
};
const mockTextureManager = new MockTextureManager(this.scene);
this.scene.add = mockTextureManager.add;
this.scene.sys.displayList = this.scene.add.displayList;
this.scene.sys.updateList = new UpdateList(this.scene);
this.scene.systems = this.scene.sys;
this.scene.input = this.game.input;
this.scene.scene = this.scene;
this.scene.input.keyboard = new KeyboardPlugin(this.scene);
this.scene.input.gamepad = new GamepadPlugin(this.scene);
this.scene.cachedFetch = (url, init) => {
return new Promise((resolve) => {
// need to remove that if later we want to test battle-anims
const newUrl = url.includes('./battle-anims/') ? prependPath('./battle-anims/tackle.json') : prependPath(url);
let raw;
try {
raw = fs.readFileSync(newUrl, {encoding: "utf8", flag: "r"});
} catch(e) {
return resolve(createFetchBadResponse({}));
}
const data = JSON.parse(raw);
const response = createFetchResponse(data);
return resolve(response);
});
};
this.scene.make = {
graphics: (config) => new MockGraphics(mockTextureManager, config),
rexTransitionImagePack: () => ({
transit: () => null,
}),
};
this.scene.time = new MockClock(this.scene);
}
}
function prependPath(originalPath) {
const prefix = "public";
if (originalPath.startsWith("./")) {
return originalPath.replace("./", `${prefix}/`);
}
return originalPath;
}
// Simulate fetch response
function createFetchResponse(data) {
return {
ok: true,
status: 200,
json: () => Promise.resolve(data),
text: () => Promise.resolve(JSON.stringify(data)),
};
}
// Simulate fetch response
function createFetchBadResponse(data) {
return {
ok: false,
status: 404,
json: () => Promise.resolve(data),
text: () => Promise.resolve(JSON.stringify(data)),
};
}

View File

@ -0,0 +1,115 @@
import BattleScene from "#app/battle-scene";
import Phaser from "phaser";
import {InputsController} from "#app/inputs-controller";
import pad_xbox360 from "#app/configs/inputs/pad_xbox360";
import {holdOn} from "#app/test/utils/gameManagerUtils";
import {initTouchControls} from "#app/touch-controls";
import { JSDOM } from "jsdom";
import fs from "fs";
export default class InputsHandler {
private scene: BattleScene;
private events: Phaser.Events.EventEmitter;
private inputController: InputsController;
public log = [];
public logUp = [];
private fakePad: Fakepad;
private fakeMobile: FakeMobile;
constructor(scene: BattleScene) {
this.scene = scene;
this.inputController = this.scene.inputController;
this.fakePad = new Fakepad(pad_xbox360);
this.fakeMobile = new FakeMobile();
this.scene.input.gamepad.gamepads.push(this.fakePad);
this.init();
}
pressTouch(button: string, duration: integer): Promise<void> {
return new Promise(async (resolve) => {
this.fakeMobile.touchDown(button);
await holdOn(duration);
this.fakeMobile.touchUp(button);
resolve();
});
}
pressGamepadButton(button: integer, duration: integer): Promise<void> {
return new Promise(async (resolve) => {
this.scene.input.gamepad.emit("down", this.fakePad, {index: button});
await holdOn(duration);
this.scene.input.gamepad.emit("up", this.fakePad, {index: button});
resolve();
});
}
pressKeyboardKey(key: integer, duration: integer): Promise<void> {
return new Promise(async (resolve) => {
this.scene.input.keyboard.emit("keydown", {keyCode: key});
await holdOn(duration);
this.scene.input.keyboard.emit("keyup", {keyCode: key});
resolve();
});
}
init(): void {
setInterval(() => {
this.inputController.update();
});
initTouchControls(this.inputController.events);
this.events = this.inputController.events;
this.scene.input.gamepad.emit("connected", this.fakePad);
this.listenInputs();
}
listenInputs(): void {
this.events.on("input_down", (event) => {
this.log.push({type: "input_down", button: event.button});
}, this);
this.events.on("input_up", (event) => {
this.logUp.push({type: "input_up", button: event.button});
}, this);
}
}
class Fakepad extends Phaser.Input.Gamepad.Gamepad {
public id: string;
public index: number;
constructor(pad) {
super(undefined, {...pad, buttons: pad.deviceMapping, axes: []});
this.id = "xbox_360_fakepad";
this.index = 0;
}
}
class FakeMobile {
constructor() {
const fakeMobilePage = fs.readFileSync("./src/test/utils/fakeMobile.html", {encoding: "utf8", flag: "r"});
const dom = new JSDOM(fakeMobilePage);
Object.defineProperty(window, "document", {
value: dom.window.document,
configurable: true,
});
}
touchDown(button: string) {
const node = document.querySelector(`[data-key][id='${button}']`);
if (!node) {
return;
}
const event = new Event("touchstart");
node.dispatchEvent(event);
}
touchUp(button: string) {
const node = document.querySelector(`[data-key][id='${button}']`);
if (!node) {
return;
}
const event = new Event("touchend");
node.dispatchEvent(event);
}
}

View File

@ -0,0 +1,79 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import {apiFetch} from "#app/utils";
import {waitUntil} from "#app/test/utils/gameManagerUtils";
describe("Test misc", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
});
it("test fetch mock async", async () => {
const spy = vi.fn();
await fetch("https://localhost:8080/account/info").then(response => {
expect(response.status).toBe(200);
expect(response.ok).toBe(true);
return response.json();
}).then(data => {
spy(); // Call the spy function
expect(data).toEqual({"username":"greenlamp","lastSessionSlot":0});
});
expect(spy).toHaveBeenCalled();
});
it("test apifetch mock async", async () => {
const spy = vi.fn();
await apiFetch("https://localhost:8080/account/info").then(response => {
expect(response.status).toBe(200);
expect(response.ok).toBe(true);
return response.json();
}).then(data => {
spy(); // Call the spy function
expect(data).toEqual({"username":"greenlamp","lastSessionSlot":0});
});
expect(spy).toHaveBeenCalled();
});
it("test fetch mock sync", async () => {
const response = await fetch("https://localhost:8080/account/info");
const data = await response.json();
expect(response.ok).toBe(true);
expect(response.status).toBe(200);
expect(data).toEqual({"username":"greenlamp","lastSessionSlot":0});
});
it("test apifetch mock sync", async () => {
const data = await game.scene.cachedFetch("./battle-anims/splishy-splash.json");
expect(data).not.toBeUndefined();
});
it("testing wait phase queue", async () => {
const fakeScene = {
phaseQueue: [1, 2, 3] // Initially not empty
};
setTimeout(() => {
fakeScene.phaseQueue = [];
}, 500);
const spy = vi.fn();
await waitUntil(() => fakeScene.phaseQueue.length === 0).then(result => {
expect(result).toBe(true);
spy(); // Call the spy function
});
expect(spy).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,17 @@
import Clock = Phaser.Time.Clock;
export class MockClock extends Clock {
constructor(scene) {
super(scene);
setInterval(() => {
/*
To simulate frame update
eventEmitter.on(SceneEvents.PRE_UPDATE, this.preUpdate, this);
eventEmitter.on(SceneEvents.UPDATE, this.update, this);
*/
this.preUpdate(this.systems.game.loop.time, 100);
this.update(this.systems.game.loop.time, 100);
}, 100);
}
}

View File

@ -0,0 +1,82 @@
const MockConsoleLog = (_logDisabled= false, _phaseText=false) => {
let logs = [];
const logDisabled: boolean = _logDisabled;
const phaseText: boolean = _phaseText;
const originalLog = console.log;
const originalError = console.error;
const originalDebug = console.debug;
const originalWarn = console.warn;
const notified = [];
const blacklist = ["Phaser", "variant icon does not exist", "Texture \"%s\" not found"];
const whitelist = ["Phase"];
return ({
log(...args) {
const argsStr = this.getStr(args);
logs.push(argsStr);
if (logDisabled && (!phaseText)) {
return;
}
if ((phaseText && !whitelist.some((b) => argsStr.includes(b))) || blacklist.some((b) => argsStr.includes(b))) {
return;
}
originalLog(args);
},
error(...args) {
const argsStr = this.getStr(args);
logs.push(argsStr);
originalError(args); // Appelle le console.error originel
},
debug(...args) {
const argsStr = this.getStr(args);
logs.push(argsStr);
if (logDisabled && (!phaseText)) {
return;
}
if (!whitelist.some((b) => argsStr.includes(b)) || blacklist.some((b) => argsStr.includes(b))) {
return;
}
originalDebug(args);
},
warn(...args) {
const argsStr = this.getStr(args);
logs.push(args);
if (logDisabled && (!phaseText)) {
return;
}
if (!whitelist.some((b) => argsStr.includes(b)) || blacklist.some((b) => argsStr.includes(b))) {
return;
}
originalWarn(args);
},
notify(msg) {
originalLog(msg);
notified.push(msg);
},
getLogs() {
return logs;
},
clearLogs() {
logs = [];
},
getStr(...args) {
return args.map(arg => {
if (typeof arg === "object" && arg !== null) {
// Handle objects including arrays
return JSON.stringify(arg, (key, value) =>
typeof value === "bigint" ? value.toString() : value
);
} else if (typeof arg === "bigint") {
// Handle BigInt values
return arg.toString();
} else {
// Handle all other types
return arg.toString();
}
}).join(";");
},
});
};
export default MockConsoleLog;

View File

@ -0,0 +1,32 @@
export const MockFetch = (input, init) => {
const url = typeof input === "string" ? input : input.url;
let responseHandler;
let responseText;
const handlers = {
"account/info": {"username":"greenlamp","lastSessionSlot":0},
"savedata/session": {},
"savedata/system": {},
"savedata/updateall": "",
"daily/rankingpagecount": { data: 0 },
"game/titlestats": {"playerCount":0,"battleCount":5},
"daily/rankings": [],
};
for (const key of Object.keys(handlers)) {
if (url.includes(key)) {
responseHandler = async() => handlers[key];
responseText = async() => handlers[key] ? JSON.stringify(handlers[key]) : handlers[key];
break;
}
}
return Promise.resolve({
ok: true,
status: 200,
json: responseHandler,
text: responseText,
});
};

View File

@ -0,0 +1,42 @@
import CacheManager = Phaser.Cache.CacheManager;
export default class MockLoader {
public cacheManager;
constructor(scene) {
this.cacheManager = new CacheManager(scene);
}
once(event, callback) {
callback();
}
setBaseURL(url) {
return null;
}
video() {
return null;
}
spritesheet(key, url, frameConfig) {
}
audio(key, url) {
}
isLoading() {
return false;
}
start() {
}
image() {
}
atlas(key, textureUrl, atlasUrl) {
}
}

View File

@ -0,0 +1,27 @@
const mockLocalStorage = (() => {
let store = {} as Storage;
return {
getItem(key: string) {
return store[key];
},
setItem(key: string, value: string) {
store[key] = value;
},
hasOwnProperty(key: string) {
return store.hasOwnProperty(key);
},
removeItem(key: string) {
delete store[key];
},
clear() {
store = {} as Storage;
},
};
});
export default mockLocalStorage;

View File

@ -0,0 +1,85 @@
import MockContainer from "#app/test/utils/mocks/mocksContainer/mockContainer";
import MockSprite from "#app/test/utils/mocks/mocksContainer/mockSprite";
import MockRectangle from "#app/test/utils/mocks/mocksContainer/mockRectangle";
import MockNineslice from "#app/test/utils/mocks/mocksContainer/mockNineslice";
import MockImage from "#app/test/utils/mocks/mocksContainer/mockImage";
import MockText from "#app/test/utils/mocks/mocksContainer/mockText";
import MockPolygon from "#app/test/utils/mocks/mocksContainer/mockPolygon";
export default class MockTextureManager {
private textures: Map<string, any>;
private scene;
public add;
public displayList;
public list = [];
constructor(scene) {
this.scene = scene;
this.textures = new Map();
this.displayList = new Phaser.GameObjects.DisplayList(scene);
this.add = {
container: this.container.bind(this),
sprite: this.sprite.bind(this),
tileSprite: this.sprite.bind(this),
existing: this.existing.bind(this),
rectangle: this.rectangle.bind(this),
nineslice: this.nineslice.bind(this),
image: this.image.bind(this),
polygon: this.polygon.bind(this),
text: this.text.bind(this),
bitmapText: this.text.bind(this),
displayList: this.displayList,
};
}
container(x, y) {
const container = new MockContainer(this, x, y);
this.list.push(container);
return container;
}
sprite(x,y, texture) {
const sprite = new MockSprite(this, x, y, texture);
this.list.push(sprite);
return sprite;
}
existing(obj) {
// const whitelist = ["ArenaBase", "PlayerPokemon", "EnemyPokemon"];
// const key = obj.constructor.name;
// if (whitelist.includes(key) || obj.texture?.key?.includes("trainer_")) {
// this.containers.push(obj);
// }
}
rectangle(x, y, width, height, fillColor) {
const rectangle = new MockRectangle(this, x, y, width, height, fillColor);
this.list.push(rectangle);
return rectangle;
}
nineslice(x, y, texture, frame, width, height, leftWidth, rightWidth, topHeight, bottomHeight) {
const nineSlice = new MockNineslice(this, x, y, texture, frame, width, height, leftWidth, rightWidth, topHeight, bottomHeight);
this.list.push(nineSlice);
return nineSlice;
}
image(x, y, texture) {
const image = new MockImage(this, x, y, texture);
this.list.push(image);
return image;
}
text(x, y, content, styleOptions) {
const text = new MockText(this, x, y, content, styleOptions);
this.list.push(text);
return text;
}
polygon(x, y, content, fillColor, fillAlpha) {
const polygon = new MockPolygon(this, x, y, content, fillColor, fillAlpha);
this.list.push(polygon);
return polygon;
}
}

View File

@ -0,0 +1,207 @@
import MockTextureManager from "#app/test/utils/mocks/mockTextureManager";
export default class MockContainer {
protected x;
protected y;
protected scene;
protected width;
protected height;
protected visible;
private alpha;
private style;
public frame;
protected textureManager;
public list = [];
constructor(textureManager: MockTextureManager, x, y) {
this.x = x;
this.y = y;
this.frame = {};
this.textureManager = textureManager;
}
setVisible(visible) {
this.visible = visible;
}
once(event, callback, source) {
}
off(event, callback, source) {
}
removeFromDisplayList() {
// same as remove or destroy
}
addedToScene() {
// This callback is invoked when this Game Object is added to a Scene.
}
setSize(width, height) {
// Sets the size of this Game Object.
}
setMask() {
/// Sets the mask that this Game Object will use to render with.
}
setPositionRelative(source, x, y) {
/// Sets the position of this Game Object to be a relative position from the source Game Object.
}
setInteractive(hitArea?, callback?, dropZone?) {
/// Sets the InteractiveObject to be a drop zone for a drag and drop operation.
}
setOrigin(x, y) {
this.x = x;
this.y = y;
}
setAlpha(alpha) {
this.alpha = alpha;
}
setFrame(frame, updateSize?: boolean, updateOrigin?: boolean) {
// Sets the frame this Game Object will use to render with.
}
setScale(scale) {
// Sets the scale of this Game Object.
}
setPosition(x, y) {
this.x = x;
this.y = y;
}
setX(x) {
this.x = x;
}
setY(y) {
this.y = y;
}
destroy() {
this.list = [];
}
setShadow(shadowXpos, shadowYpos, shadowColor) {
// Sets the shadow settings for this Game Object.
}
setLineSpacing(lineSpacing) {
// Sets the line spacing value of this Game Object.
}
setText(text) {
// Sets the text this Game Object will display.
}
setAngle(angle) {
// Sets the angle of this Game Object.
}
setShadowOffset(offsetX, offsetY) {
// Sets the shadow offset values.
}
setWordWrapWidth(width) {
// Sets the width (in pixels) to use for wrapping lines.
}
setFontSize(fontSize) {
// Sets the font size of this Game Object.
}
getBounds() {
return { width: this.width, height: this.height };
}
setColor(color) {
// Sets the tint of this Game Object.
}
setShadowColor(color) {
// Sets the shadow color.
}
setTint(color) {
// Sets the tint of this Game Object.
}
setStrokeStyle(thickness, color) {
// Sets the stroke style for the graphics.
return this;
}
setDepth(depth) {
// Sets the depth of this Game Object.
}
setTexture(texture) {
// Sets the texture this Game Object will use to render with.
}
clearTint() {
// Clears any previously set tint.
}
sendToBack() {
// Sends this Game Object to the back of its parent's display list.
}
moveAbove(obj) {
// Moves this Game Object to be above the given Game Object in the display list.
}
moveBelow(obj) {
// Moves this Game Object to be below the given Game Object in the display list.
}
setName(name) {
// return this.phaserSprite.setName(name);
}
bringToTop(obj) {
// Brings this Game Object to the top of its parents display list.
}
on(event, callback, source) {
}
add(obj) {
// Adds a child to this Game Object.
this.list.push(obj);
}
removeAll() {
// Removes all Game Objects from this Container.
this.list = [];
}
addAt(obj, index) {
// Adds a Game Object to this Container at the given index.
this.list.splice(index, 0, obj);
}
remove(obj) {
const index = this.list.indexOf(obj);
if (index !== -1) {
this.list.splice(index, 1);
}
}
getIndex(obj) {
const index = this.list.indexOf(obj);
return index || -1;
}
getAt(index) {
return this.list[index];
}
getAll() {
return this.list;
}
}

View File

@ -0,0 +1,96 @@
export default class MockGraphics {
private scene;
public list = [];
constructor(textureManager, config) {
this.scene = textureManager.scene;
}
fillStyle(color) {
// Sets the fill style to be used by the fill methods.
}
beginPath() {
// Starts a new path by emptying the list of sub-paths. Call this method when you want to create a new path.
}
fillRect(x, y, width, height) {
// Adds a rectangle shape to the path which is filled when you call fill().
}
createGeometryMask() {
// Creates a geometry mask.
}
setOrigin(x, y) {
}
setAlpha(alpha) {
}
setVisible(visible) {
}
setName(name) {
}
once(event, callback, source) {
}
removeFromDisplayList() {
// same as remove or destroy
}
addedToScene() {
// This callback is invoked when this Game Object is added to a Scene.
}
setPositionRelative(source, x, y) {
/// Sets the position of this Game Object to be a relative position from the source Game Object.
}
destroy() {
this.list = [];
}
setScale(scale) {
// Sets the scale of this Game Object.
}
off(event, callback, source) {
}
add(obj) {
// Adds a child to this Game Object.
this.list.push(obj);
}
removeAll() {
// Removes all Game Objects from this Container.
this.list = [];
}
addAt(obj, index) {
// Adds a Game Object to this Container at the given index.
this.list.splice(index, 0, obj);
}
remove(obj) {
const index = this.list.indexOf(obj);
if (index !== -1) {
this.list.splice(index, 1);
}
}
getIndex(obj) {
const index = this.list.indexOf(obj);
return index || -1;
}
getAt(index) {
return this.list[index];
}
getAll() {
return this.list;
}
}

View File

@ -0,0 +1,11 @@
import MockContainer from "#app/test/utils/mocks/mocksContainer/mockContainer";
export default class MockImage extends MockContainer {
private texture;
constructor(textureManager, x, y, texture) {
super(textureManager, x, y);
this.texture = texture;
}
}

View File

@ -0,0 +1,20 @@
import MockContainer from "#app/test/utils/mocks/mocksContainer/mockContainer";
export default class MockNineslice extends MockContainer {
private texture;
private leftWidth;
private rightWidth;
private topHeight;
private bottomHeight;
constructor(textureManager, x, y, texture, frame, width, height, leftWidth, rightWidth, topHeight, bottomHeight) {
super(textureManager, x, y);
this.texture = texture;
this.frame = frame;
this.leftWidth = leftWidth;
this.rightWidth = rightWidth;
this.topHeight = topHeight;
this.bottomHeight = bottomHeight;
}
}

View File

@ -0,0 +1,9 @@
import MockContainer from "#app/test/utils/mocks/mocksContainer/mockContainer";
export default class MockPolygon extends MockContainer {
constructor(textureManager, x, y, content, fillColor, fillAlpha) {
super(textureManager, x, y);
}
}

View File

@ -0,0 +1,74 @@
export default class MockRectangle {
private fillColor;
private scene;
public list = [];
constructor(textureManager, x, y, width, height, fillColor) {
this.fillColor = fillColor;
this.scene = textureManager.scene;
}
setOrigin(x, y) {
}
setAlpha(alpha) {
}
setVisible(visible) {
}
setName(name) {
}
once(event, callback, source) {
}
removeFromDisplayList() {
// same as remove or destroy
}
addedToScene() {
// This callback is invoked when this Game Object is added to a Scene.
}
setPositionRelative(source, x, y) {
/// Sets the position of this Game Object to be a relative position from the source Game Object.
}
destroy() {
this.list = [];
}
add(obj) {
// Adds a child to this Game Object.
this.list.push(obj);
}
removeAll() {
// Removes all Game Objects from this Container.
this.list = [];
}
addAt(obj, index) {
// Adds a Game Object to this Container at the given index.
this.list.splice(index, 0, obj);
}
remove(obj) {
const index = this.list.indexOf(obj);
if (index !== -1) {
this.list.splice(index, 1);
}
}
getIndex(obj) {
const index = this.list.indexOf(obj);
return index || -1;
}
getAt(index) {
return this.list[index];
}
getAll() {
return this.list;
}
}

View File

@ -0,0 +1,205 @@
import Sprite = Phaser.GameObjects.Sprite;
import Frame = Phaser.Textures.Frame;
import Phaser from "phaser";
export default class MockSprite {
private phaserSprite;
public pipelineData;
public texture;
public key;
public frame;
public textureManager;
public scene;
public anims;
public list = [];
constructor(textureManager, x, y, texture) {
this.textureManager = textureManager;
this.scene = textureManager.scene;
Phaser.GameObjects.Sprite.prototype.setInteractive = this.setInteractive;
// @ts-ignore
Phaser.GameObjects.Sprite.prototype.setTexture = this.setTexture;
Phaser.GameObjects.Sprite.prototype.setSizeToFrame = this.setSizeToFrame;
Phaser.GameObjects.Sprite.prototype.setFrame = this.setFrame;
// Phaser.GameObjects.Sprite.prototype.disable = this.disable;
// Phaser.GameObjects.Sprite.prototype.texture = { frameTotal: 1, get: () => null };
this.phaserSprite = new Phaser.GameObjects.Sprite(textureManager.scene, x, y, texture);
this.pipelineData = {};
this.texture = {
key: texture || "",
};
this.anims = {
pause: () => null,
};
}
setTexture(key: string, frame?: string | number) {
return this;
}
setSizeToFrame(frame?: boolean | Frame): Sprite {
return {} as Sprite;
}
setPipeline(obj) {
// Sets the pipeline of this Game Object.
return this.phaserSprite.setPipeline(obj);
}
off(event, callback, source) {
}
setTintFill(color) {
// Sets the tint fill color.
return this.phaserSprite.setTintFill(color);
}
setScale(scale) {
return this.phaserSprite.setScale(scale);
}
setOrigin(x, y) {
return this.phaserSprite.setOrigin(x, y);
}
setSize(width, height) {
// Sets the size of this Game Object.
return this.phaserSprite.setSize(width, height);
}
once(event, callback, source) {
return this.phaserSprite.once(event, callback, source);
}
removeFromDisplayList() {
// same as remove or destroy
return this.phaserSprite.removeFromDisplayList();
}
addedToScene() {
// This callback is invoked when this Game Object is added to a Scene.
return this.phaserSprite.addedToScene();
}
setVisible(visible) {
return this.phaserSprite.setVisible(visible);
}
setPosition(x, y) {
return this.phaserSprite.setPosition(x, y);
}
stop() {
return this.phaserSprite.stop();
}
setInteractive(hitArea, hitAreaCallback, dropZone) {
return null;
}
on(event, callback, source) {
return this.phaserSprite.on(event, callback, source);
}
setAlpha(alpha) {
return this.phaserSprite.setAlpha(alpha);
}
setTint(color) {
// Sets the tint of this Game Object.
return this.phaserSprite.setTint(color);
}
setFrame(frame, updateSize?: boolean, updateOrigin?: boolean) {
// Sets the frame this Game Object will use to render with.
this.frame = frame;
return frame;
}
setPositionRelative(source, x, y) {
/// Sets the position of this Game Object to be a relative position from the source Game Object.
return this.phaserSprite.setPositionRelative(source, x, y);
}
setCrop(x, y, width, height) {
// Sets the crop size of this Game Object.
return this.phaserSprite.setCrop(x, y, width, height);
}
clearTint() {
// Clears any previously set tint.
return this.phaserSprite.clearTint();
}
disableInteractive() {
// Disables Interactive features of this Game Object.
return null;
}
apply() {
return this.phaserSprite.apply();
}
play() {
// return this.phaserSprite.play();
}
setPipelineData(key, value) {
this.pipelineData[key] = value;
}
destroy() {
return this.phaserSprite.destroy();
}
setName(name) {
return this.phaserSprite.setName(name);
}
setAngle(angle) {
return this.phaserSprite.setAngle(angle);
}
setMask() {
}
add(obj) {
// Adds a child to this Game Object.
this.list.push(obj);
}
removeAll() {
// Removes all Game Objects from this Container.
this.list = [];
}
addAt(obj, index) {
// Adds a Game Object to this Container at the given index.
this.list.splice(index, 0, obj);
}
remove(obj) {
const index = this.list.indexOf(obj);
if (index !== -1) {
this.list.splice(index, 1);
}
}
getIndex(obj) {
const index = this.list.indexOf(obj);
return index || -1;
}
getAt(index) {
return this.list[index];
}
getAll() {
return this.list;
}
}

View File

@ -0,0 +1,265 @@
import UI from "#app/ui/ui";
export default class MockText {
private phaserText;
private wordWrapWidth;
private splitRegExp;
private scene;
private textureManager;
public list = [];
constructor(textureManager, x, y, content, styleOptions) {
this.scene = textureManager.scene;
this.textureManager = textureManager;
// Phaser.GameObjects.TextStyle.prototype.setStyle = () => null;
// Phaser.GameObjects.Text.prototype.updateText = () => null;
// Phaser.Textures.TextureManager.prototype.addCanvas = () => {};
UI.prototype.showText = this.showText;
// super(scene, x, y);
// this.phaserText = new Phaser.GameObjects.Text(scene, x, y, content, styleOptions);
}
runWordWrap(text) {
if (!text) {
return "";
}
let result = "";
this.splitRegExp = /(?:\r\n|\r|\n)/;
const lines = text.split(this.splitRegExp);
const lastLineIndex = lines.length - 1;
const whiteSpaceWidth = 2;
for (let i = 0; i <= lastLineIndex; i++) {
let spaceLeft = this.wordWrapWidth;
const words = lines[i].split(" ");
const lastWordIndex = words.length - 1;
for (let j = 0; j <= lastWordIndex; j++) {
const word = words[j];
const wordWidth = word.length * 2;
let wordWidthWithSpace = wordWidth;
if (j < lastWordIndex) {
wordWidthWithSpace += whiteSpaceWidth;
}
if (wordWidthWithSpace > spaceLeft) {
// Skip printing the newline if it's the first word of the line that is greater
// than the word wrap width.
if (j > 0) {
result += "\n";
spaceLeft = this.wordWrapWidth;
}
}
result += word;
if (j < lastWordIndex) {
result += " ";
spaceLeft -= wordWidthWithSpace;
} else {
spaceLeft -= wordWidth;
}
}
if (i < lastLineIndex) {
result += "\n";
}
}
return result;
}
showText(text, delay, callback, callbackDelay, prompt, promptDelay) {
this.scene.messageWrapper.showText(text, delay, callback, callbackDelay, prompt, promptDelay);
if (callback) {
callback();
}
}
setScale(scale) {
// return this.phaserText.setScale(scale);
}
setShadow(shadowXpos, shadowYpos, shadowColor) {
// Sets the shadow settings for this Game Object.
// return this.phaserText.setShadow(shadowXpos, shadowYpos, shadowColor);
}
setLineSpacing(lineSpacing) {
// Sets the line spacing value of this Game Object.
// return this.phaserText.setLineSpacing(lineSpacing);
}
setOrigin(x, y) {
// return this.phaserText.setOrigin(x, y);
}
once(event, callback, source) {
// return this.phaserText.once(event, callback, source);
}
off(event, callback, obj) {
}
removedFromScene() {
}
addToDisplayList() {
}
setStroke(color, thickness) {
// Sets the stroke color and thickness.
// return this.phaserText.setStroke(color, thickness);
}
removeFromDisplayList() {
// same as remove or destroy
// return this.phaserText.removeFromDisplayList();
}
addedToScene() {
// This callback is invoked when this Game Object is added to a Scene.
// return this.phaserText.addedToScene();
}
setVisible(visible) {
// return this.phaserText.setVisible(visible);
}
setY(y) {
// return this.phaserText.setY(y);
}
setX(x) {
// return this.phaserText.setX(x);
}
setText(text) {
// Sets the text this Game Object will display.
// return this.phaserText.setText(text);
}
setAngle(angle) {
// Sets the angle of this Game Object.
// return this.phaserText.setAngle(angle);
}
setPositionRelative(source, x, y) {
/// Sets the position of this Game Object to be a relative position from the source Game Object.
// return this.phaserText.setPositionRelative(source, x, y);
}
setShadowOffset(offsetX, offsetY) {
// Sets the shadow offset values.
// return this.phaserText.setShadowOffset(offsetX, offsetY);
}
setWordWrapWidth(width) {
// Sets the width (in pixels) to use for wrapping lines.
this.wordWrapWidth = width;
}
setFontSize(fontSize) {
// Sets the font size of this Game Object.
// return this.phaserText.setFontSize(fontSize);
}
getBounds() {
// return this.phaserText.getBounds();
return {
width: 1,
};
}
setColor(color) {
// Sets the tint of this Game Object.
// return this.phaserText.setColor(color);
}
setShadowColor(color) {
// Sets the shadow color.
// return this.phaserText.setShadowColor(color);
}
setTint(color) {
// Sets the tint of this Game Object.
// return this.phaserText.setTint(color);
}
setStrokeStyle(thickness, color) {
// Sets the stroke style for the graphics.
// return this.phaserText.setStrokeStyle(thickness, color);
}
destroy() {
// return this.phaserText.destroy();
this.list = [];
}
setAlpha(alpha) {
// return this.phaserText.setAlpha(alpha);
}
setName(name) {
// return this.phaserText.setName(name);
}
setAlign(align) {
// return this.phaserText.setAlign(align);
}
setMask() {
/// Sets the mask that this Game Object will use to render with.
}
getBottomLeft() {
return {
x: 0,
y: 0,
};
}
getTopLeft() {
return {
x: 0,
y: 0,
};
}
add(obj) {
// Adds a child to this Game Object.
this.list.push(obj);
}
removeAll() {
// Removes all Game Objects from this Container.
this.list = [];
}
addAt(obj, index) {
// Adds a Game Object to this Container at the given index.
this.list.splice(index, 0, obj);
}
remove(obj) {
const index = this.list.indexOf(obj);
if (index !== -1) {
this.list.splice(index, 1);
}
}
getIndex(obj) {
const index = this.list.indexOf(obj);
return index || -1;
}
getAt(index) {
return this.list[index];
}
getAll() {
return this.list;
}
}

View File

@ -0,0 +1,279 @@
import {
BattleEndPhase,
BerryPhase,
CheckSwitchPhase, CommandPhase, DamagePhase, EggLapsePhase,
EncounterPhase, EnemyCommandPhase, FaintPhase,
LoginPhase, MessagePhase, MoveEffectPhase, MoveEndPhase, MovePhase, NewBattlePhase, NextEncounterPhase,
PostSummonPhase,
SelectGenderPhase, SelectModifierPhase,
SelectStarterPhase, ShinySparklePhase, ShowAbilityPhase, StatChangePhase, SummonPhase,
TitlePhase, ToggleDoublePositionPhase, TurnEndPhase, TurnInitPhase, TurnStartPhase, VictoryPhase
} from "#app/phases";
import {Mode} from "#app/ui/ui";
export default class PhaseInterceptor {
public scene;
public phases = {};
public log;
private onHold;
private interval;
private promptInterval;
private intervalRun;
private prompts;
private phaseFrom;
/**
* List of phases with their corresponding start methods.
*/
private PHASES = [
[LoginPhase, this.startPhase],
[TitlePhase, this.startPhase],
[SelectGenderPhase, this.startPhase],
[EncounterPhase, this.startPhase],
[SelectStarterPhase, this.startPhase],
[PostSummonPhase, this.startPhase],
[SummonPhase, this.startPhase],
[ToggleDoublePositionPhase, this.startPhase],
[CheckSwitchPhase, this.startPhase],
[ShowAbilityPhase, this.startPhase],
[MessagePhase, this.startPhase],
[TurnInitPhase, this.startPhase],
[CommandPhase, this.startPhase],
[EnemyCommandPhase, this.startPhase],
[TurnStartPhase, this.startPhase],
[MovePhase, this.startPhase],
[MoveEffectPhase, this.startPhase],
[DamagePhase, this.startPhase],
[FaintPhase, this.startPhase],
[BerryPhase, this.startPhase],
[TurnEndPhase, this.startPhase],
[BattleEndPhase, this.startPhase],
[EggLapsePhase, this.startPhase],
[SelectModifierPhase, this.startPhase],
[NextEncounterPhase, this.startPhase],
[NewBattlePhase, this.startPhase],
[VictoryPhase, this.startPhase],
[MoveEndPhase, this.startPhase],
[StatChangePhase, this.startPhase],
[ShinySparklePhase, this.startPhase],
];
/**
* Constructor to initialize the scene and properties, and to start the phase handling.
* @param scene - The scene to be managed.
*/
constructor(scene) {
this.scene = scene;
this.log = [];
this.onHold = [];
this.prompts = [];
this.initPhases();
this.startPromptHander();
}
/**
* Method to set the starting phase.
* @param phaseFrom - The phase to start from.
* @returns The instance of the PhaseInterceptor.
*/
runFrom(phaseFrom) {
this.phaseFrom = phaseFrom;
return this;
}
/**
* Method to transition to a target phase.
* @param phaseTo - The phase to transition to.
* @returns A promise that resolves when the transition is complete.
*/
async to(phaseTo): Promise<void> {
return new Promise(async (resolve) => {
await this.run(this.phaseFrom);
this.phaseFrom = null;
const targetName = typeof phaseTo === "string" ? phaseTo : phaseTo.name;
this.intervalRun = setInterval(async () => {
const currentPhase = this.onHold?.length && this.onHold[0];
if (currentPhase && currentPhase.name !== targetName) {
await this.run(currentPhase.name);
} else if (currentPhase.name === targetName) {
await this.run(currentPhase.name);
clearInterval(this.intervalRun);
return resolve();
}
});
});
}
/**
* Method to run a phase with an optional skip function.
* @param phaseTarget - The phase to run.
* @param skipFn - Optional skip function.
* @returns A promise that resolves when the phase is run.
*/
run(phaseTarget, skipFn?): Promise<void> {
this.scene.moveAnimations = null; // Mandatory to avoid crash
return new Promise(async (resolve) => {
this.waitUntil(phaseTarget, skipFn).then(() => {
const currentPhase = this.onHold.shift();
currentPhase.call();
resolve();
}).catch(() => {
resolve();
});
});
}
/**
* Method to ensure a phase is run, to throw error on test if not.
* @param phaseTarget - The phase to run.
* @returns A promise that resolves when the phase is run.
*/
mustRun(phaseTarget): Promise<void> {
const targetName = typeof phaseTarget === "string" ? phaseTarget : phaseTarget.name;
this.scene.moveAnimations = null; // Mandatory to avoid crash
return new Promise(async (resolve, reject) => {
const interval = setInterval(async () => {
const currentPhase = this.onHold?.length && this.onHold[0];
if (currentPhase && currentPhase.name !== targetName) {
reject(currentPhase);
} else if (currentPhase && currentPhase.name === targetName) {
clearInterval(interval);
await this.run(phaseTarget);
resolve();
}
});
});
}
/**
* Method to execute actions when about to run a phase. Does not run the phase, stop right before.
* @param phaseTarget - The phase to run.
* @param skipFn - Optional skip function.
* @returns A promise that resolves when the phase is about to run.
*/
whenAboutToRun(phaseTarget, skipFn?): Promise<void> {
return new Promise(async (resolve) => {
this.waitUntil(phaseTarget, skipFn).then(() => {
resolve();
}).catch(() => {
resolve();
});
});
}
/**
* Method to remove a phase from the list.
* @param phaseTarget - The phase to remove.
* @param skipFn - Optional skip function.
* @returns A promise that resolves when the phase is removed.
*/
remove(phaseTarget, skipFn?): Promise<void> {
return new Promise(async (resolve) => {
this.waitUntil(phaseTarget, skipFn).then(() => {
this.onHold.shift();
this.scene.getCurrentPhase().end();
resolve();
}).catch(() => {
resolve();
});
});
}
/**
* Method to wait until a specific phase is reached.
* @param phaseTarget - The phase to wait for.
* @param skipFn - Optional skip function.
* @returns A promise that resolves when the phase is reached.
*/
waitUntil(phaseTarget, skipFn?): Promise<void> {
const targetName = typeof phaseTarget === "string" ? phaseTarget : phaseTarget.name;
return new Promise((resolve, reject) => {
this.interval = setInterval(() => {
const currentPhase = this.onHold?.length && this.onHold[0] && this.onHold[0].name;
// if the currentPhase here is not filled, it means it's a phase we haven't added to the list
if (currentPhase === targetName) {
clearInterval(this.interval);
return resolve();
} else if (skipFn && skipFn()) {
clearInterval(this.interval);
return reject("Skipped phase");
}
});
});
}
/**
* Method to initialize phases and their corresponding methods.
*/
initPhases() {
for (const [phase, method] of this.PHASES) {
const originalStart = phase.prototype.start;
this.phases[phase.name] = originalStart;
phase.prototype.start = () => method.call(this, phase);
}
}
/**
* Method to start a phase and log it.
* @param phase - The phase to start.
*/
startPhase(phase) {
this.log.push(phase.name);
const instance = this.scene.getCurrentPhase();
this.onHold.push({
name: phase.name,
call: () => {
this.phases[phase.name].apply(instance);
}
});
}
/**
* Method to start the prompt handler.
*/
startPromptHander() {
this.promptInterval = setInterval(() => {
if (this.prompts.length) {
const actionForNextPrompt = this.prompts[0];
const expireFn = actionForNextPrompt.expireFn && actionForNextPrompt.expireFn();
const currentMode = this.scene.ui.getMode();
const currentPhase = this.scene.getCurrentPhase().constructor.name;
if (expireFn) {
this.prompts.shift();
} else if (currentMode === actionForNextPrompt.mode && currentPhase === actionForNextPrompt.phaseTarget) {
this.prompts.shift().callback();
}
}
});
}
/**
* Method to add an action to the next prompt.
* @param phaseTarget - The target phase for the prompt.
* @param mode - The mode of the UI.
* @param callback - The callback function to execute.
* @param expireFn - The function to determine if the prompt has expired.
*/
addToNextPrompt(phaseTarget: string, mode: Mode, callback: () => void, expireFn: () => void) {
this.prompts.push({
phaseTarget,
mode,
callback,
expireFn
});
}
/**
* Restores the original state of phases and clears intervals.
*
* This function iterates through all phases and resets their `start` method to the original
* function stored in `this.phases`. Additionally, it clears the `promptInterval` and `interval`.
*/
restoreOg() {
for (const [phase] of this.PHASES) {
phase.prototype.start = this.phases[phase.name];
}
clearInterval(this.promptInterval);
clearInterval(this.interval);
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,4 @@
import "vitest-canvas-mock"; import "vitest-canvas-mock";
import "#app/test/phaser.setup";
import "#app/test/fontFace.setup"; import "#app/test/fontFace.setup";
import {initStatsKeys} from "#app/ui/game-stats-ui-handler"; import {initStatsKeys} from "#app/ui/game-stats-ui-handler";
import {initPokemonPrevolutions} from "#app/data/pokemon-evolutions"; import {initPokemonPrevolutions} from "#app/data/pokemon-evolutions";
@ -9,7 +8,9 @@ import {initPokemonForms} from "#app/data/pokemon-forms";
import {initSpecies} from "#app/data/pokemon-species"; import {initSpecies} from "#app/data/pokemon-species";
import {initMoves} from "#app/data/move"; import {initMoves} from "#app/data/move";
import {initAbilities} from "#app/data/ability"; import {initAbilities} from "#app/data/ability";
import {initAchievements} from "#app/system/achv.js";
initAchievements();
initStatsKeys(); initStatsKeys();
initPokemonPrevolutions(); initPokemonPrevolutions();
initBiomes(); initBiomes();

View File

@ -1,3 +1,4 @@
import Phaser from "phaser";
import BattleScene from "../battle-scene"; import BattleScene from "../battle-scene";
import { TextStyle, addTextObject } from "./text"; import { TextStyle, addTextObject } from "./text";
import { Mode } from "./ui"; import { Mode } from "./ui";

View File

@ -207,8 +207,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
private genSpecies: PokemonSpecies[][] = []; private genSpecies: PokemonSpecies[][] = [];
private lastSpecies: PokemonSpecies; private lastSpecies: PokemonSpecies;
private speciesLoaded: Map<Species, boolean> = new Map<Species, boolean>(); private speciesLoaded: Map<Species, boolean> = new Map<Species, boolean>();
private starterGens: integer[] = []; public starterGens: integer[] = [];
private starterCursors: integer[] = []; public starterCursors: integer[] = [];
private pokerusGens: integer[] = []; private pokerusGens: integer[] = [];
private pokerusCursors: integer[] = []; private pokerusCursors: integer[] = [];
private starterAttr: bigint[] = []; private starterAttr: bigint[] = [];
@ -227,7 +227,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
private canAddParty: boolean; private canAddParty: boolean;
private assetLoadCancelled: Utils.BooleanHolder; private assetLoadCancelled: Utils.BooleanHolder;
private cursorObj: Phaser.GameObjects.Image; public cursorObj: Phaser.GameObjects.Image;
private starterCursorObjs: Phaser.GameObjects.Image[]; private starterCursorObjs: Phaser.GameObjects.Image[];
private pokerusCursorObjs: Phaser.GameObjects.Image[]; private pokerusCursorObjs: Phaser.GameObjects.Image[];
private starterIcons: Phaser.GameObjects.Sprite[]; private starterIcons: Phaser.GameObjects.Sprite[];

View File

@ -1,9 +1,15 @@
import { expect, describe, it } from "vitest"; import {expect, describe, it, beforeAll} from "vitest";
import { randomString, padInt } from "./utils"; import { randomString, padInt } from "./utils";
import Phaser from "phaser"; import Phaser from "phaser";
describe("utils", () => { describe("utils", () => {
beforeAll(() => {
new Phaser.Game({
type: Phaser.HEADLESS,
});
});
describe("randomString", () => { describe("randomString", () => {
it("should return a string of the specified length", () => { it("should return a string of the specified length", () => {
const str = randomString(10); const str = randomString(10);

View File

@ -15,6 +15,7 @@ export default defineConfig(({ mode }) => {
}, },
threads: false, threads: false,
trace: true, trace: true,
restoreMocks: true,
environmentOptions: { environmentOptions: {
jsdom: { jsdom: {
resources: 'usable', resources: 'usable',