mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-09-23 23:13:42 +02:00
Merge remote-tracking branch 'upstream/beta' into phase-interceptor
This commit is contained in:
commit
6e0d0cd495
11
biome.jsonc
11
biome.jsonc
@ -254,16 +254,9 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Overrides to prevent unused import removal inside `overrides.ts`, enums & `.d.ts` files (for TSDoc linkcodes),
|
// Overrides to prevent unused import removal inside `overrides.ts`, enums & `.d.ts` files (for TSDoc linkcodes),
|
||||||
// as well as inside script boilerplate files.
|
// as well as inside script boilerplate files (whose imports will _presumably_ be used in the generated file).
|
||||||
{
|
{
|
||||||
// TODO: Rename existing boilerplates in the folder and remove this last alias
|
"includes": ["**/src/overrides.ts", "**/src/enums/**/*", "**/*.d.ts", "scripts/**/*.boilerplate.ts"],
|
||||||
"includes": [
|
|
||||||
"**/src/overrides.ts",
|
|
||||||
"**/src/enums/**/*",
|
|
||||||
"**/*.d.ts",
|
|
||||||
"scripts/**/*.boilerplate.ts",
|
|
||||||
"**/boilerplates/*.ts"
|
|
||||||
],
|
|
||||||
"linter": {
|
"linter": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"correctness": {
|
"correctness": {
|
||||||
|
@ -43,6 +43,6 @@ describe("{{description}}", () => {
|
|||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expect(feebas).toHaveUsedMove({ move: MoveId.SPLASH, result: MoveResult.SUCCESS });
|
expect(feebas).toHaveUsedMove({ move: MoveId.SPLASH, result: MoveResult.SUCCESS });
|
||||||
expect(game.textInterceptor.logs).toContain(i18next.t("moveTriggers:splash"));
|
expect(game).toHaveShownMessage(i18next.t("moveTriggers:splash"));
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -102,9 +102,9 @@ async function promptFileName(selectedType) {
|
|||||||
function getBoilerplatePath(choiceType) {
|
function getBoilerplatePath(choiceType) {
|
||||||
switch (choiceType) {
|
switch (choiceType) {
|
||||||
// case "Reward":
|
// case "Reward":
|
||||||
// return path.join(__dirname, "boilerplates/reward.ts");
|
// return path.join(__dirname, "boilerplates/reward.boilerplate.ts");
|
||||||
default:
|
default:
|
||||||
return path.join(__dirname, "boilerplates/default.ts");
|
return path.join(__dirname, "boilerplates/default.boilerplate.ts");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { speciesEggMoves } from "#balance/egg-moves";
|
import { speciesEggMoves } from "#balance/egg-moves";
|
||||||
import {
|
import {
|
||||||
|
BASE_LEVEL_WEIGHT_OFFSET,
|
||||||
BASE_WEIGHT_MULTIPLIER,
|
BASE_WEIGHT_MULTIPLIER,
|
||||||
BOSS_EXTRA_WEIGHT_MULTIPLIER,
|
BOSS_EXTRA_WEIGHT_MULTIPLIER,
|
||||||
COMMON_TIER_TM_LEVEL_REQUIREMENT,
|
COMMON_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
@ -72,7 +73,7 @@ function getAndWeightLevelMoves(pokemon: Pokemon): Map<MoveId, number> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let weight = learnLevel + 20;
|
let weight = learnLevel + BASE_LEVEL_WEIGHT_OFFSET;
|
||||||
switch (learnLevel) {
|
switch (learnLevel) {
|
||||||
case EVOLVE_MOVE:
|
case EVOLVE_MOVE:
|
||||||
weight = EVOLUTION_MOVE_WEIGHT;
|
weight = EVOLUTION_MOVE_WEIGHT;
|
||||||
@ -132,6 +133,11 @@ function getTmPoolForSpecies(
|
|||||||
): void {
|
): void {
|
||||||
const [allowCommon, allowGreat, allowUltra] = allowedTiers;
|
const [allowCommon, allowGreat, allowUltra] = allowedTiers;
|
||||||
const tms = speciesTmMoves[speciesId];
|
const tms = speciesTmMoves[speciesId];
|
||||||
|
// Species with no learnable TMs (e.g. Ditto) don't have entries in the `speciesTmMoves` object,
|
||||||
|
// so this is needed to avoid iterating over `undefined`
|
||||||
|
if (tms == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let moveId: MoveId;
|
let moveId: MoveId;
|
||||||
for (const tm of tms) {
|
for (const tm of tms) {
|
||||||
@ -241,7 +247,11 @@ function getEggPoolForSpecies(
|
|||||||
excludeRare: boolean,
|
excludeRare: boolean,
|
||||||
rareEggMoveWeight = 0,
|
rareEggMoveWeight = 0,
|
||||||
): void {
|
): void {
|
||||||
for (const [idx, moveId] of speciesEggMoves[rootSpeciesId].entries()) {
|
const eggMoves = speciesEggMoves[rootSpeciesId];
|
||||||
|
if (eggMoves == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [idx, moveId] of eggMoves.entries()) {
|
||||||
if (levelPool.has(moveId) || (idx === 3 && excludeRare)) {
|
if (levelPool.has(moveId) || (idx === 3 && excludeRare)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -416,7 +426,6 @@ function adjustDamageMoveWeights(pool: Map<MoveId, number>, pokemon: Pokemon, wi
|
|||||||
* @param pool - The move pool to calculate the total weight for
|
* @param pool - The move pool to calculate the total weight for
|
||||||
* @returns The total weight of all moves in the pool
|
* @returns The total weight of all moves in the pool
|
||||||
*/
|
*/
|
||||||
// biome-ignore lint/correctness/noUnusedVariables: May be useful
|
|
||||||
function calculateTotalPoolWeight(pool: Map<MoveId, number>): number {
|
function calculateTotalPoolWeight(pool: Map<MoveId, number>): number {
|
||||||
let totalWeight = 0;
|
let totalWeight = 0;
|
||||||
for (const weight of pool.values()) {
|
for (const weight of pool.values()) {
|
||||||
@ -622,7 +631,7 @@ function fillInRemainingMovesetSlots(
|
|||||||
* @param note - Short note to include in the log for context
|
* @param note - Short note to include in the log for context
|
||||||
*/
|
*/
|
||||||
function debugMoveWeights(pokemon: Pokemon, pool: Map<MoveId, number>, note: string): void {
|
function debugMoveWeights(pokemon: Pokemon, pool: Map<MoveId, number>, note: string): void {
|
||||||
if (isBeta || import.meta.env.DEV) {
|
if ((isBeta || import.meta.env.DEV) && import.meta.env.NODE_ENV !== "test") {
|
||||||
const moveNameToWeightMap = new Map<string, number>();
|
const moveNameToWeightMap = new Map<string, number>();
|
||||||
const sortedByValue = Array.from(pool.entries()).sort((a, b) => b[1] - a[1]);
|
const sortedByValue = Array.from(pool.entries()).sort((a, b) => b[1] - a[1]);
|
||||||
for (const [moveId, weight] of sortedByValue) {
|
for (const [moveId, weight] of sortedByValue) {
|
||||||
@ -713,3 +722,49 @@ export function generateMoveset(pokemon: Pokemon): void {
|
|||||||
filterPool(baseWeights, (m: MoveId) => !pokemon.moveset.some(mo => m[0] === mo.moveId)),
|
filterPool(baseWeights, (m: MoveId) => !pokemon.moveset.some(mo => m[0] === mo.moveId)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports for internal testing purposes.
|
||||||
|
* ⚠️ These *must not* be used outside of tests, as they will not be defined.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const __INTERNAL_TEST_EXPORTS: {
|
||||||
|
getAndWeightLevelMoves: typeof getAndWeightLevelMoves;
|
||||||
|
getAllowedTmTiers: typeof getAllowedTmTiers;
|
||||||
|
getTmPoolForSpecies: typeof getTmPoolForSpecies;
|
||||||
|
getAndWeightTmMoves: typeof getAndWeightTmMoves;
|
||||||
|
getEggMoveWeight: typeof getEggMoveWeight;
|
||||||
|
getEggPoolForSpecies: typeof getEggPoolForSpecies;
|
||||||
|
getAndWeightEggMoves: typeof getAndWeightEggMoves;
|
||||||
|
filterMovePool: typeof filterMovePool;
|
||||||
|
adjustWeightsForTrainer: typeof adjustWeightsForTrainer;
|
||||||
|
adjustDamageMoveWeights: typeof adjustDamageMoveWeights;
|
||||||
|
calculateTotalPoolWeight: typeof calculateTotalPoolWeight;
|
||||||
|
filterPool: typeof filterPool;
|
||||||
|
forceStabMove: typeof forceStabMove;
|
||||||
|
filterRemainingTrainerMovePool: typeof filterRemainingTrainerMovePool;
|
||||||
|
fillInRemainingMovesetSlots: typeof fillInRemainingMovesetSlots;
|
||||||
|
} = {} as any;
|
||||||
|
|
||||||
|
// We can't use `import.meta.vitest` here, because this would not be set
|
||||||
|
// until the tests themselves begin to run, which is after imports
|
||||||
|
// So we rely on NODE_ENV being test instead
|
||||||
|
if (import.meta.env.NODE_ENV === "test") {
|
||||||
|
Object.assign(__INTERNAL_TEST_EXPORTS, {
|
||||||
|
getAndWeightLevelMoves,
|
||||||
|
getAllowedTmTiers,
|
||||||
|
getTmPoolForSpecies,
|
||||||
|
getAndWeightTmMoves,
|
||||||
|
getEggMoveWeight,
|
||||||
|
getEggPoolForSpecies,
|
||||||
|
getAndWeightEggMoves,
|
||||||
|
filterMovePool,
|
||||||
|
adjustWeightsForTrainer,
|
||||||
|
adjustDamageMoveWeights,
|
||||||
|
calculateTotalPoolWeight,
|
||||||
|
filterPool,
|
||||||
|
forceStabMove,
|
||||||
|
filterRemainingTrainerMovePool,
|
||||||
|
fillInRemainingMovesetSlots,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -1346,13 +1346,12 @@ export class BattleScene extends SceneBase {
|
|||||||
if (newBattleType === BattleType.TRAINER) {
|
if (newBattleType === BattleType.TRAINER) {
|
||||||
const trainerType =
|
const trainerType =
|
||||||
Overrides.RANDOM_TRAINER_OVERRIDE?.trainerType ?? this.arena.randomTrainerType(newWaveIndex);
|
Overrides.RANDOM_TRAINER_OVERRIDE?.trainerType ?? this.arena.randomTrainerType(newWaveIndex);
|
||||||
|
const hasDouble = trainerConfigs[trainerType].hasDouble;
|
||||||
let doubleTrainer = false;
|
let doubleTrainer = false;
|
||||||
if (trainerConfigs[trainerType].doubleOnly) {
|
if (trainerConfigs[trainerType].doubleOnly) {
|
||||||
doubleTrainer = true;
|
doubleTrainer = true;
|
||||||
} else if (trainerConfigs[trainerType].hasDouble) {
|
} else if (hasDouble) {
|
||||||
doubleTrainer =
|
doubleTrainer = !randSeedInt(this.getDoubleBattleChance(newWaveIndex, playerField));
|
||||||
Overrides.RANDOM_TRAINER_OVERRIDE?.alwaysDouble
|
|
||||||
|| !randSeedInt(this.getDoubleBattleChance(newWaveIndex, playerField));
|
|
||||||
// Add a check that special trainers can't be double except for tate and liza - they should use the normal double chance
|
// Add a check that special trainers can't be double except for tate and liza - they should use the normal double chance
|
||||||
if (
|
if (
|
||||||
trainerConfigs[trainerType].trainerTypeDouble
|
trainerConfigs[trainerType].trainerTypeDouble
|
||||||
@ -1361,11 +1360,19 @@ export class BattleScene extends SceneBase {
|
|||||||
doubleTrainer = false;
|
doubleTrainer = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const variant = doubleTrainer
|
|
||||||
? TrainerVariant.DOUBLE
|
// Forcing a double battle on wave 1 causes a bug where only one enemy is sent out,
|
||||||
: randSeedInt(2)
|
// making it impossible to complete the fight without a reload
|
||||||
? TrainerVariant.FEMALE
|
const overrideVariant =
|
||||||
: TrainerVariant.DEFAULT;
|
Overrides.RANDOM_TRAINER_OVERRIDE?.trainerVariant === TrainerVariant.DOUBLE
|
||||||
|
&& (!hasDouble || newWaveIndex <= 1)
|
||||||
|
? TrainerVariant.DEFAULT
|
||||||
|
: Overrides.RANDOM_TRAINER_OVERRIDE?.trainerVariant;
|
||||||
|
|
||||||
|
const variant =
|
||||||
|
overrideVariant
|
||||||
|
?? (doubleTrainer ? TrainerVariant.DOUBLE : randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT);
|
||||||
|
|
||||||
newTrainer = trainerData !== undefined ? trainerData.toTrainer() : new Trainer(trainerType, variant);
|
newTrainer = trainerData !== undefined ? trainerData.toTrainer() : new Trainer(trainerType, variant);
|
||||||
this.field.add(newTrainer);
|
this.field.add(newTrainer);
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,33 @@ export const GREAT_TM_MOVESET_WEIGHT = 14;
|
|||||||
/** The weight given to TMs in the ultra tier during moveset generation */
|
/** The weight given to TMs in the ultra tier during moveset generation */
|
||||||
export const ULTRA_TM_MOVESET_WEIGHT = 18;
|
export const ULTRA_TM_MOVESET_WEIGHT = 18;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base weight offset for level moves
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* The relative likelihood of moves learned at different levels is determined by
|
||||||
|
* the ratio of their weights,
|
||||||
|
* or, the formula:
|
||||||
|
* `(levelB + BASE_LEVEL_WEIGHT_OFFSET) / (levelA + BASE_LEVEL_WEIGHT_OFFSET)`
|
||||||
|
*
|
||||||
|
* For example, consider move A and B that are learned at levels 1 and 60, respectively,
|
||||||
|
* but have no other differences (same power, accuracy, category, etc).
|
||||||
|
* The following table demonstrates the likelihood of move B being chosen over move A.
|
||||||
|
*
|
||||||
|
* | Offset | Likelihood |
|
||||||
|
* |--------|------------|
|
||||||
|
* | 0 | 60x |
|
||||||
|
* | 1 | 30x |
|
||||||
|
* | 5 | 10.8x |
|
||||||
|
* | 20 | 3.8x |
|
||||||
|
* | 60 | 2x |
|
||||||
|
*
|
||||||
|
* Note that increasing this without adjusting the other weights will decrease the likelihood of non-level moves
|
||||||
|
*
|
||||||
|
* For a complete picture, see {@link https://www.desmos.com/calculator/wgln4dxigl}
|
||||||
|
*/
|
||||||
|
export const BASE_LEVEL_WEIGHT_OFFSET = 20;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The maximum weight an egg move can ever have
|
* The maximum weight an egg move can ever have
|
||||||
* @remarks
|
* @remarks
|
||||||
|
@ -18,6 +18,7 @@ import { Stat } from "#enums/stat";
|
|||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
import { TimeOfDay } from "#enums/time-of-day";
|
import { TimeOfDay } from "#enums/time-of-day";
|
||||||
import { TrainerType } from "#enums/trainer-type";
|
import { TrainerType } from "#enums/trainer-type";
|
||||||
|
import { TrainerVariant } from "#enums/trainer-variant";
|
||||||
import { Unlockables } from "#enums/unlockables";
|
import { Unlockables } from "#enums/unlockables";
|
||||||
import { VariantTier } from "#enums/variant-tier";
|
import { VariantTier } from "#enums/variant-tier";
|
||||||
import { WeatherType } from "#enums/weather-type";
|
import { WeatherType } from "#enums/weather-type";
|
||||||
@ -311,8 +312,12 @@ export type BattleStyle = "double" | "single" | "even-doubles" | "odd-doubles";
|
|||||||
export type RandomTrainerOverride = {
|
export type RandomTrainerOverride = {
|
||||||
/** The Type of trainer to force */
|
/** The Type of trainer to force */
|
||||||
trainerType: Exclude<TrainerType, TrainerType.UNKNOWN>;
|
trainerType: Exclude<TrainerType, TrainerType.UNKNOWN>;
|
||||||
/* If the selected trainer type has a double version, it will always use its double version. */
|
/**
|
||||||
alwaysDouble?: boolean;
|
* The {@linkcode TrainerVariant} to force.
|
||||||
|
* @remarks
|
||||||
|
* `TrainerVariant.DOUBLE` cannot be forced on the first wave of a game due to issues with trainer party generation.
|
||||||
|
*/
|
||||||
|
trainerVariant?: TrainerVariant;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The type of the {@linkcode DefaultOverrides} class */
|
/** The type of the {@linkcode DefaultOverrides} class */
|
||||||
|
@ -33,7 +33,7 @@ export class LoginPhase extends Phase {
|
|||||||
globalScene.ui.showText(i18next.t("menu:logInOrCreateAccount"));
|
globalScene.ui.showText(i18next.t("menu:logInOrCreateAccount"));
|
||||||
}
|
}
|
||||||
|
|
||||||
globalScene.playSound("menu_open");
|
globalScene.playSound("ui/menu_open");
|
||||||
|
|
||||||
const loadData = () => {
|
const loadData = () => {
|
||||||
updateUserInfo().then(success => {
|
updateUserInfo().then(success => {
|
||||||
@ -53,7 +53,7 @@ export class LoginPhase extends Phase {
|
|||||||
loadData();
|
loadData();
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
globalScene.playSound("menu_open");
|
globalScene.playSound("ui/menu_open");
|
||||||
globalScene.ui.setMode(UiMode.REGISTRATION_FORM, {
|
globalScene.ui.setMode(UiMode.REGISTRATION_FORM, {
|
||||||
buttonActions: [
|
buttonActions: [
|
||||||
() => {
|
() => {
|
||||||
|
@ -23,11 +23,13 @@ export function getCookie(cName: string): string {
|
|||||||
}
|
}
|
||||||
const name = `${cName}=`;
|
const name = `${cName}=`;
|
||||||
const ca = document.cookie.split(";");
|
const ca = document.cookie.split(";");
|
||||||
// Check all cookies in the document and see if any of them match, grabbing the first one whose value lines up
|
for (let c of ca) {
|
||||||
for (const c of ca) {
|
// ⚠️ DO NOT REPLACE THIS WITH C = C.TRIM() - IT BREAKS IN NON-CHROMIUM BROWSERS ⚠️
|
||||||
const cTrimmed = c.trim();
|
while (c.charAt(0) === " ") {
|
||||||
if (cTrimmed.startsWith(name)) {
|
c = c.substring(1);
|
||||||
return c.slice(name.length, c.length);
|
}
|
||||||
|
if (c.indexOf(name) === 0) {
|
||||||
|
return c.substring(name.length, c.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
|
3
src/vite.env.d.ts
vendored
3
src/vite.env.d.ts
vendored
@ -9,8 +9,9 @@ interface ImportMetaEnv {
|
|||||||
readonly VITE_DISCORD_CLIENT_ID?: string;
|
readonly VITE_DISCORD_CLIENT_ID?: string;
|
||||||
readonly VITE_GOOGLE_CLIENT_ID?: string;
|
readonly VITE_GOOGLE_CLIENT_ID?: string;
|
||||||
readonly VITE_I18N_DEBUG?: string;
|
readonly VITE_I18N_DEBUG?: string;
|
||||||
|
readonly NODE_ENV?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
declare interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv;
|
readonly env: ImportMetaEnv;
|
||||||
}
|
}
|
||||||
|
285
test/ai/ai-moveset-gen.test.ts
Normal file
285
test/ai/ai-moveset-gen.test.ts
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
import { __INTERNAL_TEST_EXPORTS } from "#app/ai/ai-moveset-gen";
|
||||||
|
import {
|
||||||
|
COMMON_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
GREAT_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
ULTRA_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
} from "#balance/moveset-generation";
|
||||||
|
import { allMoves, allSpecies } from "#data/data-lists";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { TrainerSlot } from "#enums/trainer-slot";
|
||||||
|
import { EnemyPokemon } from "#field/pokemon";
|
||||||
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import { NumberHolder } from "#utils/common";
|
||||||
|
import { afterEach } from "node:test";
|
||||||
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for {@linkcode createTestablePokemon}
|
||||||
|
*/
|
||||||
|
interface MockPokemonParams {
|
||||||
|
/** The level to set the Pokémon to */
|
||||||
|
level: number;
|
||||||
|
/**
|
||||||
|
* Whether the pokemon is a boss or not.
|
||||||
|
* @defaultValue `false`
|
||||||
|
*/
|
||||||
|
boss?: boolean;
|
||||||
|
/**
|
||||||
|
* The trainer slot to assign to the pokemon, if any.
|
||||||
|
* @defaultValue `TrainerSlot.NONE`
|
||||||
|
*/
|
||||||
|
trainerSlot?: TrainerSlot;
|
||||||
|
/**
|
||||||
|
* The form index to assign to the pokemon, if any.
|
||||||
|
* This *must* be one of the valid form indices for the species, or the test will break.
|
||||||
|
* @defaultValue `0`
|
||||||
|
*/
|
||||||
|
formIndex?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an `EnemyPokemon` that can be used for testing
|
||||||
|
* @param species - The species ID of the pokemon to create
|
||||||
|
* @returns The newly created `EnemyPokemon`.
|
||||||
|
* @todo Move this to a dedicated unit test util folder if more tests come to rely on it
|
||||||
|
*/
|
||||||
|
function createTestablePokemon(
|
||||||
|
species: SpeciesId,
|
||||||
|
{ level, trainerSlot = TrainerSlot.NONE, boss = false, formIndex = 0 }: MockPokemonParams,
|
||||||
|
): EnemyPokemon {
|
||||||
|
const pokemon = new EnemyPokemon(allSpecies[species], level, trainerSlot, boss);
|
||||||
|
if (formIndex !== 0) {
|
||||||
|
const formIndexLength = allSpecies[species]?.forms.length;
|
||||||
|
const name = allSpecies[species]?.name;
|
||||||
|
expect(formIndex, `${name} does not have a form with index ${formIndex}`).toBeLessThan(formIndexLength);
|
||||||
|
pokemon.formIndex = formIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pokemon;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Unit Tests - ai-moveset-gen.ts", () => {
|
||||||
|
describe("filterPool", () => {
|
||||||
|
const { filterPool } = __INTERNAL_TEST_EXPORTS;
|
||||||
|
it("clones a pool when there are no predicates", () => {
|
||||||
|
const pool = new Map<MoveId, number>([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const filtered = filterPool(pool, () => true);
|
||||||
|
const expected = [
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
];
|
||||||
|
expect(filtered).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not modify the original pool", () => {
|
||||||
|
const pool = new Map<MoveId, number>([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
]);
|
||||||
|
const original = new Map(pool);
|
||||||
|
|
||||||
|
filterPool(pool, moveId => moveId !== MoveId.TACKLE);
|
||||||
|
expect(pool).toEqual(original);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters out moves that do not match the predicate", () => {
|
||||||
|
const pool = new Map<MoveId, number>([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
[MoveId.SPLASH, 3],
|
||||||
|
]);
|
||||||
|
const filtered = filterPool(pool, moveId => moveId !== MoveId.SPLASH);
|
||||||
|
expect(filtered).toEqual([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty array if no moves match the predicate", () => {
|
||||||
|
const pool = new Map<MoveId, number>([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
]);
|
||||||
|
const filtered = filterPool(pool, () => false);
|
||||||
|
expect(filtered).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calculates totalWeight correctly when provided", () => {
|
||||||
|
const pool = new Map<MoveId, number>([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
[MoveId.SPLASH, 3],
|
||||||
|
]);
|
||||||
|
const totalWeight = new NumberHolder(0);
|
||||||
|
const filtered = filterPool(pool, moveId => moveId !== MoveId.SPLASH, totalWeight);
|
||||||
|
expect(filtered).toEqual([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
]);
|
||||||
|
expect(totalWeight.value).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Clears totalWeight when provided", () => {
|
||||||
|
const pool = new Map<MoveId, number>([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
]);
|
||||||
|
const totalWeight = new NumberHolder(42);
|
||||||
|
const filtered = filterPool(pool, () => false, totalWeight);
|
||||||
|
expect(filtered).toEqual([]);
|
||||||
|
expect(totalWeight.value).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAllowedTmTiers", () => {
|
||||||
|
const { getAllowedTmTiers } = __INTERNAL_TEST_EXPORTS;
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ tierName: "common", resIdx: 0, level: COMMON_TIER_TM_LEVEL_REQUIREMENT - 1 },
|
||||||
|
{ tierName: "great", resIdx: 1, level: GREAT_TIER_TM_LEVEL_REQUIREMENT - 1 },
|
||||||
|
{ tierName: "ultra", resIdx: 2, level: ULTRA_TIER_TM_LEVEL_REQUIREMENT - 1 },
|
||||||
|
])("should prevent $name TMs when below level $level", ({ level, resIdx }) => {
|
||||||
|
expect(getAllowedTmTiers(level)[resIdx]).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ tierName: "common", resIdx: 0, level: COMMON_TIER_TM_LEVEL_REQUIREMENT },
|
||||||
|
{ tierName: "great", resIdx: 1, level: GREAT_TIER_TM_LEVEL_REQUIREMENT },
|
||||||
|
{ tierName: "ultra", resIdx: 2, level: ULTRA_TIER_TM_LEVEL_REQUIREMENT },
|
||||||
|
])("should allow $name TMs when at level $level", ({ level, resIdx }) => {
|
||||||
|
expect(getAllowedTmTiers(level)[resIdx]).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unit tests for methods that require a game context
|
||||||
|
describe("", () => {
|
||||||
|
//#region boilerplate
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
/**A pokemon object that will be cleaned up after every test */
|
||||||
|
let pokemon: EnemyPokemon | null = null;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
// Game manager can be reused between tests as we are not really modifying the global state
|
||||||
|
// So there is no need to put this in a beforeEach with cleanup in afterEach.
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
pokemon?.destroy();
|
||||||
|
});
|
||||||
|
// Sanitize the interceptor after running the suite to ensure other tests are not affected
|
||||||
|
afterAll(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
//#endregion boilerplate
|
||||||
|
|
||||||
|
function createCharmander(_ = pokemon): asserts _ is EnemyPokemon {
|
||||||
|
pokemon?.destroy();
|
||||||
|
pokemon = createTestablePokemon(SpeciesId.CHARMANDER, { level: 10 });
|
||||||
|
expect(pokemon).toBeInstanceOf(EnemyPokemon);
|
||||||
|
}
|
||||||
|
describe("getAndWeightLevelMoves", () => {
|
||||||
|
const { getAndWeightLevelMoves } = __INTERNAL_TEST_EXPORTS;
|
||||||
|
|
||||||
|
it("returns an empty map if getLevelMoves throws", async () => {
|
||||||
|
createCharmander(pokemon);
|
||||||
|
vi.spyOn(pokemon, "getLevelMoves").mockImplementation(() => {
|
||||||
|
throw new Error("fail");
|
||||||
|
});
|
||||||
|
// Suppress the warning from the test output
|
||||||
|
const warnMock = vi.spyOn(console, "warn").mockImplementationOnce(() => {});
|
||||||
|
|
||||||
|
const result = getAndWeightLevelMoves(pokemon);
|
||||||
|
expect(warnMock).toHaveBeenCalled();
|
||||||
|
expect(result.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips unimplemented moves", () => {
|
||||||
|
createCharmander(pokemon);
|
||||||
|
vi.spyOn(pokemon, "getLevelMoves").mockReturnValue([
|
||||||
|
[1, MoveId.TACKLE],
|
||||||
|
[5, MoveId.GROWL],
|
||||||
|
]);
|
||||||
|
vi.spyOn(allMoves[MoveId.TACKLE], "name", "get").mockReturnValue("Tackle (N)");
|
||||||
|
const result = getAndWeightLevelMoves(pokemon);
|
||||||
|
expect(result.has(MoveId.TACKLE)).toBe(false);
|
||||||
|
expect(result.has(MoveId.GROWL)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips moves already in the pool", () => {
|
||||||
|
createCharmander(pokemon);
|
||||||
|
vi.spyOn(pokemon, "getLevelMoves").mockReturnValue([
|
||||||
|
[1, MoveId.TACKLE],
|
||||||
|
[5, MoveId.TACKLE],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = getAndWeightLevelMoves(pokemon);
|
||||||
|
expect(result.get(MoveId.TACKLE)).toBe(21);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("weights moves based on level", () => {
|
||||||
|
createCharmander(pokemon);
|
||||||
|
vi.spyOn(pokemon, "getLevelMoves").mockReturnValue([
|
||||||
|
[1, MoveId.TACKLE],
|
||||||
|
[5, MoveId.GROWL],
|
||||||
|
[9, MoveId.EMBER],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = getAndWeightLevelMoves(pokemon);
|
||||||
|
expect(result.get(MoveId.TACKLE)).toBe(21);
|
||||||
|
expect(result.get(MoveId.GROWL)).toBe(25);
|
||||||
|
expect(result.get(MoveId.EMBER)).toBe(29);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Regression Tests - ai-moveset-gen.ts", () => {
|
||||||
|
//#region boilerplate
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
/**A pokemon object that will be cleaned up after every test */
|
||||||
|
let pokemon: EnemyPokemon | null = null;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
// Game manager can be reused between tests as we are not really modifying the global state
|
||||||
|
// So there is no need to put this in a beforeEach with cleanup in afterEach.
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
pokemon?.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
//#endregion boilerplate
|
||||||
|
|
||||||
|
describe("getTmPoolForSpecies", () => {
|
||||||
|
const { getTmPoolForSpecies } = __INTERNAL_TEST_EXPORTS;
|
||||||
|
|
||||||
|
it("should not crash when generating a moveset for Pokemon without TM moves", () => {
|
||||||
|
pokemon = createTestablePokemon(SpeciesId.DITTO, { level: 50 });
|
||||||
|
expect(() =>
|
||||||
|
getTmPoolForSpecies(SpeciesId.DITTO, ULTRA_TIER_TM_LEVEL_REQUIREMENT, "", new Map(), new Map(), new Map(), [
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
]),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -10,6 +10,7 @@ import { PokemonType } from "#enums/pokemon-type";
|
|||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
import { TrainerType } from "#enums/trainer-type";
|
import { TrainerType } from "#enums/trainer-type";
|
||||||
|
import { TrainerVariant } from "#enums/trainer-variant";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
@ -193,7 +194,7 @@ describe("Moves - Whirlwind", () => {
|
|||||||
.battleType(BattleType.TRAINER)
|
.battleType(BattleType.TRAINER)
|
||||||
.randomTrainer({
|
.randomTrainer({
|
||||||
trainerType: TrainerType.BREEDER,
|
trainerType: TrainerType.BREEDER,
|
||||||
alwaysDouble: true,
|
trainerVariant: TrainerVariant.DOUBLE,
|
||||||
})
|
})
|
||||||
.enemyMoveset([MoveId.SPLASH, MoveId.LUNAR_DANCE])
|
.enemyMoveset([MoveId.SPLASH, MoveId.LUNAR_DANCE])
|
||||||
.moveset([MoveId.WHIRLWIND, MoveId.SPLASH]);
|
.moveset([MoveId.WHIRLWIND, MoveId.SPLASH]);
|
||||||
|
Loading…
Reference in New Issue
Block a user