Merge branch 'beta' into CosmogEvoChange

This commit is contained in:
Blitzy 2025-08-02 16:34:28 -05:00 committed by GitHub
commit 750323bc6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 96 additions and 116 deletions

View File

@ -380,9 +380,21 @@ export class BattleScene extends SceneBase {
};
}
populateAnims();
/**
* These moves serve as fallback animations for other moves without loaded animations, and
* must be loaded prior to game start.
*/
const defaultMoves = [MoveId.TACKLE, MoveId.TAIL_WHIP, MoveId.FOCUS_ENERGY, MoveId.STRUGGLE];
await this.initVariantData();
await Promise.all([
populateAnims(),
this.initVariantData(),
initCommonAnims().then(() => loadCommonAnimAssets(true)),
Promise.all(defaultMoves.map(m => initMoveAnim(m))).then(() => loadMoveAnimAssets(defaultMoves, true)),
this.initStarterColors(),
]).catch(reason => {
throw new Error(`Unexpected error during BattleScene preLoad!\nReason: ${reason}`);
});
}
create() {
@ -584,8 +596,6 @@ export class BattleScene extends SceneBase {
this.party = [];
const loadPokemonAssets = [];
this.arenaPlayer = new ArenaBase(true);
this.arenaPlayer.setName("arena-player");
this.arenaPlayerTransition = new ArenaBase(true);
@ -640,26 +650,14 @@ export class BattleScene extends SceneBase {
this.reset(false, false, true);
// Initialize UI-related aspects and then start the login phase.
const ui = new UI();
this.uiContainer.add(ui);
this.ui = ui;
ui.setup();
const defaultMoves = [MoveId.TACKLE, MoveId.TAIL_WHIP, MoveId.FOCUS_ENERGY, MoveId.STRUGGLE];
Promise.all([
Promise.all(loadPokemonAssets),
initCommonAnims().then(() => loadCommonAnimAssets(true)),
Promise.all(
[MoveId.TACKLE, MoveId.TAIL_WHIP, MoveId.FOCUS_ENERGY, MoveId.STRUGGLE].map(m => initMoveAnim(m)),
).then(() => loadMoveAnimAssets(defaultMoves, true)),
this.initStarterColors(),
]).then(() => {
this.phaseManager.toTitleScreen(true);
this.phaseManager.shiftPhase();
});
this.phaseManager.toTitleScreen(true);
this.phaseManager.shiftPhase();
}
initSession(): void {

View File

@ -4,7 +4,6 @@ import { defaultStarterSpecies } from "#app/constants";
import { globalScene } from "#app/global-scene";
import { pokemonEvolutions } from "#balance/pokemon-evolutions";
import { speciesStarterCosts } from "#balance/starters";
import { getEggTierForSpecies } from "#data/egg";
import { pokemonFormChanges } from "#data/pokemon-forms";
import type { PokemonSpecies } from "#data/pokemon-species";
import { getPokemonSpeciesForm } from "#data/pokemon-species";
@ -12,7 +11,6 @@ import { BattleType } from "#enums/battle-type";
import { ChallengeType } from "#enums/challenge-type";
import { Challenges } from "#enums/challenges";
import { TypeColor, TypeShadow } from "#enums/color";
import { EggTier } from "#enums/egg-type";
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
import { ModifierTier } from "#enums/modifier-tier";
import type { MoveId } from "#enums/move-id";
@ -26,7 +24,7 @@ import type { Pokemon } from "#field/pokemon";
import { Trainer } from "#field/trainer";
import { PokemonMove } from "#moves/pokemon-move";
import type { DexAttrProps, GameData } from "#system/game-data";
import { BooleanHolder, type NumberHolder, randSeedItem } from "#utils/common";
import { BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common";
import { deepCopy } from "#utils/data";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import { toCamelCase, toSnakeCase } from "#utils/strings";
@ -685,14 +683,11 @@ export class SingleTypeChallenge extends Challenge {
*/
export class FreshStartChallenge extends Challenge {
constructor() {
super(Challenges.FRESH_START, 3);
super(Challenges.FRESH_START, 2);
}
applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder): boolean {
if (
(this.value === 1 && !defaultStarterSpecies.includes(pokemon.speciesId)) ||
(this.value === 2 && getEggTierForSpecies(pokemon) >= EggTier.EPIC)
) {
if (this.value === 1 && !defaultStarterSpecies.includes(pokemon.speciesId)) {
valid.value = false;
return true;
}
@ -708,12 +703,18 @@ export class FreshStartChallenge extends Challenge {
pokemon.abilityIndex = pokemon.abilityIndex % 2; // Always base ability, if you set it to hidden it wraps to first ability
pokemon.passive = false; // Passive isn't unlocked
pokemon.nature = Nature.HARDY; // Neutral nature
pokemon.moveset = pokemon.species
let validMoves = pokemon.species
.getLevelMoves()
.filter(m => m[0] <= 5)
.map(lm => lm[1])
.slice(0, 4)
.map(m => new PokemonMove(m)); // No egg moves
.filter(m => isBetween(m[0], 1, 5))
.map(lm => lm[1]);
// Filter egg moves out of the moveset
pokemon.moveset = pokemon.moveset.filter(pm => validMoves.includes(pm.moveId));
if (pokemon.moveset.length < 4) {
// If there's empty slots fill with remaining valid moves
const existingMoveIds = pokemon.moveset.map(pm => pm.moveId);
validMoves = validMoves.filter(m => !existingMoveIds.includes(m));
pokemon.moveset = pokemon.moveset.concat(validMoves.map(m => new PokemonMove(m))).slice(0, 4);
}
pokemon.luck = 0; // No luck
pokemon.shiny = false; // Not shiny
pokemon.variant = 0; // Not shiny

View File

@ -99,8 +99,12 @@ export class SelectStarterPhase extends Phase {
starterPokemon.generateFusionSpecies(true);
}
starterPokemon.setVisible(false);
applyChallenges(ChallengeType.STARTER_MODIFY, starterPokemon);
const chalApplied = applyChallenges(ChallengeType.STARTER_MODIFY, starterPokemon);
party.push(starterPokemon);
if (chalApplied) {
// If any challenges modified the starter, it should update
loadPokemonAssets.push(starterPokemon.updateInfo());
}
loadPokemonAssets.push(starterPokemon.loadAssets());
});
overrideModifiers();

View File

@ -1,5 +1,4 @@
import { GameManager } from "#test/test-utils/game-manager";
import { waitUntil } from "#test/test-utils/game-manager-utils";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@ -36,19 +35,6 @@ describe("Test misc", () => {
expect(spy).toHaveBeenCalled();
});
// it.skip("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();
@ -62,19 +48,4 @@ describe("Test misc", () => {
const data = await game.scene.cachedFetch("./battle-anims/splishy-splash.json");
expect(data).toBeDefined();
});
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

@ -87,17 +87,6 @@ function getTestRunStarters(seed: string, species?: SpeciesId[]): Starter[] {
return starters;
}
export function waitUntil(truth): Promise<unknown> {
return new Promise(resolve => {
const interval = setInterval(() => {
if (truth()) {
clearInterval(interval);
resolve(true);
}
}, 1000);
});
}
/**
* Useful for populating party, wave index, etc. without having to spin up and run through an entire EncounterPhase
*/

View File

@ -31,7 +31,7 @@ import { TurnEndPhase } from "#phases/turn-end-phase";
import { TurnInitPhase } from "#phases/turn-init-phase";
import { TurnStartPhase } from "#phases/turn-start-phase";
import { ErrorInterceptor } from "#test/test-utils/error-interceptor";
import { generateStarter, waitUntil } from "#test/test-utils/game-manager-utils";
import { generateStarter } from "#test/test-utils/game-manager-utils";
import { GameWrapper } from "#test/test-utils/game-wrapper";
import { ChallengeModeHelper } from "#test/test-utils/helpers/challenge-mode-helper";
import { ClassicModeHelper } from "#test/test-utils/helpers/classic-mode-helper";
@ -85,30 +85,22 @@ export class GameManager {
constructor(phaserGame: Phaser.Game, bypassLogin = true) {
localStorage.clear();
ErrorInterceptor.getInstance().clear();
BattleScene.prototype.randBattleSeedInt = (range, min = 0) => min + range - 1; // This simulates a max roll
// Simulate max rolls on RNG functions
// TODO: Create helpers for disabling/enabling battle RNG
BattleScene.prototype.randBattleSeedInt = (range, min = 0) => min + range - 1;
this.gameWrapper = new GameWrapper(phaserGame, bypassLogin);
let firstTimeScene = false;
// TODO: Figure out a way to optimize and re-use the same game manager for each test
// Re-use an existing `globalScene` if present, or else create a new scene from scratch.
if (globalScene) {
this.scene = globalScene;
this.phaseInterceptor = new PhaseInterceptor(this.scene);
this.resetScene();
} else {
this.scene = new BattleScene();
this.phaseInterceptor = new PhaseInterceptor(this.scene);
this.gameWrapper.setScene(this.scene);
firstTimeScene = true;
}
this.phaseInterceptor = new PhaseInterceptor(this.scene);
if (!firstTimeScene) {
this.scene.reset(false, true);
(this.scene.ui.handlers[UiMode.STARTER_SELECT] as StarterSelectUiHandler).clearStarterPreferences();
// Must be run after phase interceptor has been initialized.
this.scene.phaseManager.toTitleScreen(true);
this.scene.phaseManager.shiftPhase();
this.gameWrapper.scene = this.scene;
}
this.textInterceptor = new TextInterceptor(this.scene);
@ -122,10 +114,30 @@ export class GameManager {
this.modifiers = new ModifierHelper(this);
this.field = new FieldHelper(this);
this.initDefaultOverrides();
// TODO: remove `any` assertion
global.fetch = vi.fn(MockFetch) as any;
}
/** Reset a prior `BattleScene` instance to the proper initial state. */
private resetScene(): void {
this.scene.reset(false, true);
(this.scene.ui.handlers[UiMode.STARTER_SELECT] as StarterSelectUiHandler).clearStarterPreferences();
this.gameWrapper.scene = this.scene;
this.scene.phaseManager.toTitleScreen(true);
this.scene.phaseManager.shiftPhase();
}
/**
* Initialize various default overrides for starting tests, typically to alleviate randomness.
*/
// TODO: This should not be here
private initDefaultOverrides(): void {
// Disables Mystery Encounters on all tests (can be overridden at test level)
this.override.mysteryEncounterChance(0);
global.fetch = vi.fn(MockFetch) as any;
}
/**
@ -141,15 +153,13 @@ export class GameManager {
* @param mode - The mode to wait for.
* @returns A promise that resolves when the mode is set.
*/
waitMode(mode: UiMode): Promise<void> {
return new Promise(async resolve => {
await waitUntil(() => this.scene.ui?.getMode() === mode);
return resolve();
});
// TODO: This is unused
async waitMode(mode: UiMode): Promise<void> {
await vi.waitUntil(() => this.scene.ui?.getMode() === mode);
}
/**
* Ends the current phase.
* End the currently running phase immediately.
*/
endPhase() {
this.scene.phaseManager.getCurrentPhase()?.end();
@ -283,11 +293,14 @@ export class GameManager {
.getPokemon()
.getMoveset()
[movePosition].getMove();
if (!move.isMultiTarget()) {
handler.setCursor(targetIndex !== undefined ? targetIndex : BattlerIndex.ENEMY);
}
if (move.isMultiTarget() && targetIndex !== undefined) {
expect.fail(`targetIndex was passed to selectMove() but move ("${move.name}") is not targetted`);
// Multi target attacks do not select a target
if (move.isMultiTarget()) {
if (targetIndex !== undefined) {
expect.fail(`targetIndex was passed to selectMove() but move ("${move.name}") is not targeted`);
}
} else {
handler.setCursor(targetIndex ?? BattlerIndex.ENEMY);
}
handler.processInput(Button.ACTION);
},

View File

@ -143,7 +143,7 @@ export class MoveHelper extends GameManagerHelper {
}
}
/** Helper function to get the index of the selected move in the selected part member's moveset. */
/** Helper function to get the index of the selected move in the selected party member's moveset. */
private getMovePosition(pokemonIndex: BattlerIndex.PLAYER | BattlerIndex.PLAYER_2, move: MoveId): number {
const playerPokemon = this.game.scene.getPlayerField()[pokemonIndex];
const moveset = playerPokemon.getMoveset();
@ -153,17 +153,18 @@ export class MoveHelper extends GameManagerHelper {
}
/**
* Modifies a player pokemon's moveset to contain only the selected move and then
* Modifies a player pokemon's moveset to contain only the selected move, and then
* selects it to be used during the next {@linkcode CommandPhase}.
*
* Warning: Will disable the player moveset override if it is enabled!
* **Warning**: Will disable the player moveset override if it is enabled, as well as any mid-battle moveset changes!
*
* Note: If you need to check for changes in the player's moveset as part of the test, it may be
* best to use {@linkcode changeMoveset} and {@linkcode select} instead.
* @param moveId - the move to use
* @param pkmIndex - The {@linkcode BattlerIndex} of the player Pokemon using the move. Relevant for double battles only and defaults to {@linkcode BattlerIndex.PLAYER} if not specified.
* @param targetIndex - The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves; should be omitted for multi-target moves.
* @param useTera - If `true`, the Pokemon will attempt to Terastallize even without a Tera Orb; default `false`.
* @param moveId - The {@linkcode MoveId} to use
* @param pkmIndex - The {@linkcode BattlerIndex} of the player Pokemon using the move. Relevant for double battles only and defaults to {@linkcode BattlerIndex.PLAYER} if not specified
* @param targetIndex - The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves; should be omitted for multi-target moves
* @param useTera - If `true`, the Pokemon will attempt to Terastallize even without a Tera Orb; default `false`
* @remarks
* If you need to check for changes in the player's moveset as part of the test, it may be
* better to use {@linkcode changeMoveset} and {@linkcode select} instead.
*/
public use(
moveId: MoveId,
@ -176,8 +177,11 @@ export class MoveHelper extends GameManagerHelper {
console.warn("Warning: `MoveHelper.use` overwriting player pokemon moveset and disabling moveset override!");
}
// Clear out both the normal and temporary movesets before setting the move.
const pokemon = this.game.scene.getPlayerField()[pkmIndex];
pokemon.moveset = [new PokemonMove(moveId)];
pokemon.moveset.splice(0);
pokemon.summonData.moveset?.splice(0);
pokemon.setMove(0, moveId);
if (useTera) {
this.selectWithTera(moveId, pkmIndex, targetIndex);
@ -211,7 +215,7 @@ export class MoveHelper extends GameManagerHelper {
/**
* Changes a pokemon's moveset to the given move(s).
*
* Used when the normal moveset override can't be used (such as when it's necessary to check or update properties of the moveset).
* Useful when normal moveset overrides can't be used (such as when it's necessary to check or update properties of the moveset).
*
* **Note**: Will disable the moveset override matching the pokemon's party.
* @param pokemon - The {@linkcode Pokemon} being modified
@ -232,8 +236,8 @@ export class MoveHelper extends GameManagerHelper {
moveset = coerceArray(moveset);
expect(moveset.length, "Cannot assign more than 4 moves to a moveset!").toBeLessThanOrEqual(4);
pokemon.moveset = [];
moveset.forEach(move => {
pokemon.moveset.push(new PokemonMove(move));
moveset.forEach((move, i) => {
pokemon.setMove(i, move);
});
const movesetStr = moveset.map(moveId => MoveId[moveId]).join(", ");
console.log(`Pokemon ${pokemon.species.name}'s moveset manually set to ${movesetStr} (=[${moveset.join(", ")}])!`);