Merge branch 'beta' into rest

This commit is contained in:
Bertie690 2025-07-17 19:05:07 -04:00 committed by GitHub
commit b3eea02779
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 2012 additions and 1688 deletions

View File

@ -11,7 +11,7 @@ on:
jobs:
deploy:
if: github.repository == 'pagefaultgames/pokerogue' && github.ref_name == ${{ vars.BETA_DEPLOY_BRANCH || 'beta' }}
if: github.repository == 'pagefaultgames/pokerogue' && github.ref_name == (vars.BETA_DEPLOY_BRANCH || 'beta')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

View File

@ -29,34 +29,34 @@
"devDependencies": {
"@biomejs/biome": "2.0.0",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.13.14",
"@vitest/coverage-istanbul": "^3.0.9",
"@types/node": "^22.16.3",
"@vitest/coverage-istanbul": "^3.2.4",
"chalk": "^5.4.1",
"dependency-cruiser": "^16.3.10",
"inquirer": "^12.4.2",
"jsdom": "^26.0.0",
"lefthook": "^1.11.5",
"msw": "^2.7.3",
"dependency-cruiser": "^16.10.4",
"inquirer": "^12.7.0",
"jsdom": "^26.1.0",
"lefthook": "^1.12.2",
"msw": "^2.10.4",
"phaser3spectorjs": "^0.0.8",
"typedoc": "^0.28.1",
"typescript": "^5.8.2",
"vite": "^6.3.4",
"typedoc": "^0.28.7",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.9",
"vitest": "^3.2.4",
"vitest-canvas-mock": "^0.3.3"
},
"dependencies": {
"@material/material-color-utilities": "^0.2.7",
"compare-versions": "^6.1.1",
"crypto-js": "^4.2.0",
"i18next": "^24.2.2",
"i18next-browser-languagedetector": "^8.0.4",
"i18next": "^24.2.3",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"i18next-korean-postposition-processor": "^1.0.0",
"json-stable-stringify": "^1.2.0",
"json-stable-stringify": "^1.3.0",
"jszip": "^3.10.1",
"phaser": "^3.88.2",
"phaser3-rex-plugins": "^1.80.15"
"phaser": "^3.90.0",
"phaser3-rex-plugins": "^1.80.16"
},
"engines": {
"node": ">=22.0.0"

File diff suppressed because it is too large Load Diff

18
src/@types/enum-types.ts Normal file
View File

@ -0,0 +1,18 @@
/** Union type accepting any TS Enum or `const object`, with or without reverse mapping. */
export type EnumOrObject = Record<string | number, string | number>;
/**
* Utility type to extract the enum values from a `const object`,
* or convert an `enum` interface produced by `typeof Enum` into the union type representing its values.
*/
export type EnumValues<E> = E[keyof E];
/**
* Generic type constraint representing a TS numeric enum with reverse mappings.
* @example
* TSNumericEnum<typeof WeatherType>
*/
export type TSNumericEnum<T extends EnumOrObject> = number extends EnumValues<T> ? T : never;
/** Generic type constraint representing a non reverse-mapped TS enum or `const object`. */
export type NormalEnum<T extends EnumOrObject> = Exclude<T, TSNumericEnum<T>>;

View File

@ -2,8 +2,8 @@ import type { PartyMemberStrength } from "#enums/party-member-strength";
import type { SpeciesId } from "#enums/species-id";
import type { EnemyPokemon } from "#field/pokemon";
import type { PersistentModifier } from "#modifiers/modifier";
import type { TrainerPartyTemplate } from "#trainers/TrainerPartyTemplate";
import type { TrainerConfig } from "#trainers/trainer-config";
import type { TrainerPartyTemplate } from "#trainers/trainer-party-template";
export type PartyTemplateFunc = () => TrainerPartyTemplate;
export type PartyMemberFunc = (level: number, strength: PartyMemberStrength) => EnemyPokemon;

View File

@ -2,8 +2,11 @@
* A collection of custom utility types that aid in type checking and ensuring strict type conformity
*/
// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment
import type { AbAttr } from "#types/ability-types";
// biome-ignore-start lint/correctness/noUnusedImports: Used in a tsdoc comment
import type { AbAttr } from "#abilities/ability";
// biome-ignore-end lint/correctness/noUnusedImports: Used in a tsdoc comment
import type { EnumValues } from "#types/enum-types";
/**
* Exactly matches the type of the argument, preventing adding additional properties.
@ -11,7 +14,7 @@ import type { AbAttr } from "#types/ability-types";
* Should never be used with `extends`, as this will nullify the exactness of the type.
*
* As an example, used to ensure that the parameters of {@linkcode AbAttr.canApply} and {@linkcode AbAttr.getTriggerMessage} are compatible with
* the type of the apply method
* the type of its {@linkcode AbAttr.apply | apply} method.
*
* @typeParam T - The type to match exactly
*/
@ -26,9 +29,18 @@ export type Exact<T> = {
export type Closed<X> = X;
/**
* Remove `readonly` from all properties of the provided type
* @typeParam T - The type to make mutable
* Remove `readonly` from all properties of the provided type.
* @typeParam T - The type to make mutable.
*/
export type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
/**
* Type helper to obtain the keys associated with a given value inside a `const object`.
* @typeParam O - The type of the object
* @typeParam V - The type of one of O's values
*/
export type InferKeys<O extends Record<keyof any, unknown>, V extends EnumValues<O>> = {
[K in keyof O]: O[K] extends V ? K : never;
}[keyof O];

View File

@ -140,7 +140,6 @@ import {
type Constructor,
fixedInt,
formatMoney,
getEnumValues,
getIvsFromId,
isBetween,
isNullOrUndefined,
@ -150,6 +149,7 @@ import {
shiftCharCodes,
} from "#utils/common";
import { deepMergeSpriteData } from "#utils/data";
import { getEnumValues } from "#utils/enums";
import { getModifierPoolForType, getModifierType } from "#utils/modifier-utils";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import i18next from "i18next";
@ -2178,6 +2178,7 @@ export class BattleScene extends SceneBase {
),
]
: allSpecies.filter(s => s.isCatchable());
// TODO: should this use `randSeedItem`?
return filteredSpecies[randSeedInt(filteredSpecies.length)];
}
@ -2203,6 +2204,7 @@ export class BattleScene extends SceneBase {
}
}
// TODO: should this use `randSeedItem`?
return biomes[randSeedInt(biomes.length)];
}
@ -3726,6 +3728,7 @@ export class BattleScene extends SceneBase {
console.log("No Mystery Encounters found, falling back to Mysterious Challengers.");
return allMysteryEncounters[MysteryEncounterType.MYSTERIOUS_CHALLENGERS];
}
// TODO: should this use `randSeedItem`?
encounter = availableEncounters[randSeedInt(availableEncounters.length)];
// New encounter object to not dirty flags
encounter = new MysteryEncounter(encounter);

View File

@ -5,12 +5,9 @@ import { BattleSpec } from "#enums/battle-spec";
import { BattleType } from "#enums/battle-type";
import { BattlerIndex } from "#enums/battler-index";
import type { Command } from "#enums/command";
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
import { ModifierTier } from "#enums/modifier-tier";
import type { MoveId } from "#enums/move-id";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PlayerGender } from "#enums/player-gender";
import type { PokeballType } from "#enums/pokeball";
import { SpeciesFormKey } from "#enums/species-form-key";
import { SpeciesId } from "#enums/species-id";
@ -26,7 +23,6 @@ import { MusicPreference } from "#system/settings";
import { trainerConfigs } from "#trainers/trainer-config";
import type { TurnMove } from "#types/turn-move";
import {
getEnumValues,
NumberHolder,
randInt,
randomString,
@ -35,6 +31,7 @@ import {
randSeedItem,
shiftCharCodes,
} from "#utils/common";
import { getEnumValues } from "#utils/enums";
export interface TurnCommand {
command: Command;
@ -577,369 +574,3 @@ export function getRandomTrainerFunc(
return new Trainer(trainerTypes[rand], trainerGender);
};
}
export interface FixedBattleConfigs {
[key: number]: FixedBattleConfig;
}
/**
* Youngster/Lass on 5
* Rival on 8, 55, 95, 145, 195
* Evil team grunts on 35, 62, 64, and 112
* Evil team admin on 66 and 114
* Evil leader on 115, 165
* E4 on 182, 184, 186, 188
* Champion on 190
*/
export const classicFixedBattles: FixedBattleConfigs = {
[ClassicFixedBossWaves.TOWN_YOUNGSTER]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
() => new Trainer(TrainerType.YOUNGSTER, randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT),
),
[ClassicFixedBossWaves.RIVAL_1]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
() =>
new Trainer(
TrainerType.RIVAL,
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
),
),
[ClassicFixedBossWaves.RIVAL_2]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
() =>
new Trainer(
TrainerType.RIVAL_2,
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
),
)
.setCustomModifierRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT],
allowLuckUpgrades: false,
}),
[ClassicFixedBossWaves.EVIL_GRUNT_1]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
getRandomTrainerFunc(
[
TrainerType.ROCKET_GRUNT,
TrainerType.MAGMA_GRUNT,
TrainerType.AQUA_GRUNT,
TrainerType.GALACTIC_GRUNT,
TrainerType.PLASMA_GRUNT,
TrainerType.FLARE_GRUNT,
TrainerType.AETHER_GRUNT,
TrainerType.SKULL_GRUNT,
TrainerType.MACRO_GRUNT,
TrainerType.STAR_GRUNT,
],
true,
),
),
[ClassicFixedBossWaves.RIVAL_3]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
() =>
new Trainer(
TrainerType.RIVAL_3,
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
),
)
.setCustomModifierRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT],
allowLuckUpgrades: false,
}),
[ClassicFixedBossWaves.EVIL_GRUNT_2]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(
getRandomTrainerFunc(
[
TrainerType.ROCKET_GRUNT,
TrainerType.MAGMA_GRUNT,
TrainerType.AQUA_GRUNT,
TrainerType.GALACTIC_GRUNT,
TrainerType.PLASMA_GRUNT,
TrainerType.FLARE_GRUNT,
TrainerType.AETHER_GRUNT,
TrainerType.SKULL_GRUNT,
TrainerType.MACRO_GRUNT,
TrainerType.STAR_GRUNT,
],
true,
),
),
[ClassicFixedBossWaves.EVIL_GRUNT_3]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(
getRandomTrainerFunc(
[
TrainerType.ROCKET_GRUNT,
TrainerType.MAGMA_GRUNT,
TrainerType.AQUA_GRUNT,
TrainerType.GALACTIC_GRUNT,
TrainerType.PLASMA_GRUNT,
TrainerType.FLARE_GRUNT,
TrainerType.AETHER_GRUNT,
TrainerType.SKULL_GRUNT,
TrainerType.MACRO_GRUNT,
TrainerType.STAR_GRUNT,
],
true,
),
),
[ClassicFixedBossWaves.EVIL_ADMIN_1]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(
getRandomTrainerFunc(
[
[TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL],
[TrainerType.TABITHA, TrainerType.COURTNEY],
[TrainerType.MATT, TrainerType.SHELLY],
[TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN],
[TrainerType.ZINZOLIN, TrainerType.COLRESS],
[TrainerType.XEROSIC, TrainerType.BRYONY],
TrainerType.FABA,
TrainerType.PLUMERIA,
TrainerType.OLEANA,
[TrainerType.GIACOMO, TrainerType.MELA, TrainerType.ATTICUS, TrainerType.ORTEGA, TrainerType.ERI],
],
true,
),
),
[ClassicFixedBossWaves.RIVAL_4]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
() =>
new Trainer(
TrainerType.RIVAL_4,
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
),
)
.setCustomModifierRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA],
allowLuckUpgrades: false,
}),
[ClassicFixedBossWaves.EVIL_GRUNT_4]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(
getRandomTrainerFunc(
[
TrainerType.ROCKET_GRUNT,
TrainerType.MAGMA_GRUNT,
TrainerType.AQUA_GRUNT,
TrainerType.GALACTIC_GRUNT,
TrainerType.PLASMA_GRUNT,
TrainerType.FLARE_GRUNT,
TrainerType.AETHER_GRUNT,
TrainerType.SKULL_GRUNT,
TrainerType.MACRO_GRUNT,
TrainerType.STAR_GRUNT,
],
true,
),
),
[ClassicFixedBossWaves.EVIL_ADMIN_2]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(
getRandomTrainerFunc(
[
[TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL],
[TrainerType.TABITHA, TrainerType.COURTNEY],
[TrainerType.MATT, TrainerType.SHELLY],
[TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN],
[TrainerType.ZINZOLIN, TrainerType.COLRESS],
[TrainerType.XEROSIC, TrainerType.BRYONY],
TrainerType.FABA,
TrainerType.PLUMERIA,
TrainerType.OLEANA,
[TrainerType.GIACOMO, TrainerType.MELA, TrainerType.ATTICUS, TrainerType.ORTEGA, TrainerType.ERI],
],
true,
1,
),
),
[ClassicFixedBossWaves.EVIL_BOSS_1]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(
getRandomTrainerFunc([
TrainerType.ROCKET_BOSS_GIOVANNI_1,
TrainerType.MAXIE,
TrainerType.ARCHIE,
TrainerType.CYRUS,
TrainerType.GHETSIS,
TrainerType.LYSANDRE,
TrainerType.LUSAMINE,
TrainerType.GUZMA,
TrainerType.ROSE,
TrainerType.PENNY,
]),
)
.setCustomModifierRewards({
guaranteedModifierTiers: [
ModifierTier.ROGUE,
ModifierTier.ROGUE,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
],
allowLuckUpgrades: false,
}),
[ClassicFixedBossWaves.RIVAL_5]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
() =>
new Trainer(
TrainerType.RIVAL_5,
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
),
)
.setCustomModifierRewards({
guaranteedModifierTiers: [
ModifierTier.ROGUE,
ModifierTier.ROGUE,
ModifierTier.ROGUE,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
],
allowLuckUpgrades: false,
}),
[ClassicFixedBossWaves.EVIL_BOSS_2]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(
getRandomTrainerFunc([
TrainerType.ROCKET_BOSS_GIOVANNI_2,
TrainerType.MAXIE_2,
TrainerType.ARCHIE_2,
TrainerType.CYRUS_2,
TrainerType.GHETSIS_2,
TrainerType.LYSANDRE_2,
TrainerType.LUSAMINE_2,
TrainerType.GUZMA_2,
TrainerType.ROSE_2,
TrainerType.PENNY_2,
]),
)
.setCustomModifierRewards({
guaranteedModifierTiers: [
ModifierTier.ROGUE,
ModifierTier.ROGUE,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
],
allowLuckUpgrades: false,
}),
[ClassicFixedBossWaves.ELITE_FOUR_1]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
getRandomTrainerFunc([
TrainerType.LORELEI,
TrainerType.WILL,
TrainerType.SIDNEY,
TrainerType.AARON,
TrainerType.SHAUNTAL,
TrainerType.MALVA,
[TrainerType.HALA, TrainerType.MOLAYNE],
TrainerType.MARNIE_ELITE,
TrainerType.RIKA,
TrainerType.CRISPIN,
]),
),
[ClassicFixedBossWaves.ELITE_FOUR_2]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.ELITE_FOUR_1)
.setGetTrainerFunc(
getRandomTrainerFunc([
TrainerType.BRUNO,
TrainerType.KOGA,
TrainerType.PHOEBE,
TrainerType.BERTHA,
TrainerType.MARSHAL,
TrainerType.SIEBOLD,
TrainerType.OLIVIA,
TrainerType.NESSA_ELITE,
TrainerType.POPPY,
TrainerType.AMARYS,
]),
),
[ClassicFixedBossWaves.ELITE_FOUR_3]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.ELITE_FOUR_1)
.setGetTrainerFunc(
getRandomTrainerFunc([
TrainerType.AGATHA,
TrainerType.BRUNO,
TrainerType.GLACIA,
TrainerType.FLINT,
TrainerType.GRIMSLEY,
TrainerType.WIKSTROM,
TrainerType.ACEROLA,
[TrainerType.BEA_ELITE, TrainerType.ALLISTER_ELITE],
TrainerType.LARRY_ELITE,
TrainerType.LACEY,
]),
),
[ClassicFixedBossWaves.ELITE_FOUR_4]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.ELITE_FOUR_1)
.setGetTrainerFunc(
getRandomTrainerFunc([
TrainerType.LANCE,
TrainerType.KAREN,
TrainerType.DRAKE,
TrainerType.LUCIAN,
TrainerType.CAITLIN,
TrainerType.DRASNA,
TrainerType.KAHILI,
TrainerType.RAIHAN_ELITE,
TrainerType.HASSEL,
TrainerType.DRAYTON,
]),
),
[ClassicFixedBossWaves.CHAMPION]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.ELITE_FOUR_1)
.setGetTrainerFunc(
getRandomTrainerFunc([
TrainerType.BLUE,
[TrainerType.RED, TrainerType.LANCE_CHAMPION],
[TrainerType.STEVEN, TrainerType.WALLACE],
TrainerType.CYNTHIA,
[TrainerType.ALDER, TrainerType.IRIS],
TrainerType.DIANTHA,
[TrainerType.KUKUI, TrainerType.HAU],
[TrainerType.LEON, TrainerType.MUSTARD],
[TrainerType.GEETA, TrainerType.NEMONA],
TrainerType.KIERAN,
]),
),
[ClassicFixedBossWaves.RIVAL_6]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
() =>
new Trainer(
TrainerType.RIVAL_6,
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
),
)
.setCustomModifierRewards({
guaranteedModifierTiers: [
ModifierTier.ROGUE,
ModifierTier.ROGUE,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
ModifierTier.GREAT,
ModifierTier.GREAT,
],
allowLuckUpgrades: false,
}),
};

View File

@ -5,7 +5,8 @@ import { PokemonType } from "#enums/pokemon-type";
import { SpeciesId } from "#enums/species-id";
import { TimeOfDay } from "#enums/time-of-day";
import { TrainerType } from "#enums/trainer-type";
import { getEnumValues, randSeedInt } from "#utils/common";
import { randSeedInt } from "#utils/common";
import { getEnumValues } from "#utils/enums";
import i18next from "i18next";
export function getBiomeName(biome: BiomeId | -1) {
@ -489,7 +490,7 @@ export const biomePokemonPools: BiomePokemonPools = {
[TimeOfDay.NIGHT]: [],
[TimeOfDay.ALL]: [ SpeciesId.POLITOED, SpeciesId.GALAR_STUNFISK ]
},
[BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.AZELF, SpeciesId.POIPOLE ] },
[BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.AZELF, { 1: [ SpeciesId.POIPOLE ], 60: [ SpeciesId.NAGANADEL ] } ] },
[BiomePoolTier.BOSS]: {
[TimeOfDay.DAWN]: [ SpeciesId.QUAGSIRE, SpeciesId.LUDICOLO ],
[TimeOfDay.DAY]: [ SpeciesId.QUAGSIRE, SpeciesId.LUDICOLO ],
@ -504,7 +505,7 @@ export const biomePokemonPools: BiomePokemonPools = {
[TimeOfDay.NIGHT]: [],
[TimeOfDay.ALL]: [ SpeciesId.FERALIGATR, SpeciesId.POLITOED, SpeciesId.SWAMPERT, SpeciesId.GALAR_STUNFISK ]
},
[BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.AZELF, SpeciesId.NAGANADEL ] },
[BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.AZELF, { 1: [ SpeciesId.POIPOLE ], 60: [ SpeciesId.NAGANADEL ] } ] },
[BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [] }
},
[BiomeId.BEACH]: {
@ -1117,7 +1118,7 @@ export const biomePokemonPools: BiomePokemonPools = {
},
[BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.LUCARIO, SpeciesId.THROH, SpeciesId.SAWK, { 1: [ SpeciesId.PANCHAM ], 52: [ SpeciesId.PANGORO ] } ] },
[BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.GALAR_FARFETCHD ] },
[BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, SpeciesId.KUBFU, SpeciesId.GALAR_ZAPDOS ] },
[BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU] }, SpeciesId.GALAR_ZAPDOS ] },
[BiomePoolTier.BOSS]: {
[TimeOfDay.DAWN]: [],
[TimeOfDay.DAY]: [],
@ -1126,7 +1127,7 @@ export const biomePokemonPools: BiomePokemonPools = {
[TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.HARIYAMA, SpeciesId.MEDICHAM, SpeciesId.LUCARIO, SpeciesId.TOXICROAK, SpeciesId.THROH, SpeciesId.SAWK, SpeciesId.SCRAFTY, SpeciesId.MIENSHAO, SpeciesId.BEWEAR, SpeciesId.GRAPPLOCT, SpeciesId.ANNIHILAPE ]
},
[BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.PANGORO, SpeciesId.SIRFETCHD, SpeciesId.HISUI_DECIDUEYE ] },
[BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, SpeciesId.URSHIFU ] },
[BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU] } ] },
[BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ZAMAZENTA, SpeciesId.GALAR_ZAPDOS ] }
},
[BiomeId.FACTORY]: {
@ -1417,8 +1418,8 @@ export const biomePokemonPools: BiomePokemonPools = {
{ 1: [ SpeciesId.HATENNA ], 32: [ SpeciesId.HATTREM ], 42: [ SpeciesId.HATTERENE ] }
]
},
[BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.AUDINO, SpeciesId.ETERNAL_FLOETTE ] },
[BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [] },
[BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.AUDINO ] },
[BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ETERNAL_FLOETTE ] },
[BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.DIANCIE, SpeciesId.ENAMORUS ] },
[BiomePoolTier.BOSS]: {
[TimeOfDay.DAWN]: [],
@ -1595,10 +1596,10 @@ export const biomePokemonPools: BiomePokemonPools = {
[BiomePoolTier.UNCOMMON]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [ SpeciesId.SOLOSIS ], 32: [ SpeciesId.DUOSION ], 41: [ SpeciesId.REUNICLUS ] } ] },
[BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.DITTO, { 1: [ SpeciesId.PORYGON ], 30: [ SpeciesId.PORYGON2 ] } ] },
[BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM ] },
[BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TYPE_NULL ] },
[BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [SpeciesId.TYPE_NULL], 60: [ SpeciesId.SILVALLY ] } ] },
[BiomePoolTier.BOSS]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MUK, SpeciesId.ELECTRODE, SpeciesId.BRONZONG, SpeciesId.MAGNEZONE, SpeciesId.PORYGON_Z, SpeciesId.REUNICLUS, SpeciesId.KLINKLANG ] },
[BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [] },
[BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM, SpeciesId.ZYGARDE, SpeciesId.SILVALLY ] },
[BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM, SpeciesId.ZYGARDE, { 1: [SpeciesId.TYPE_NULL], 60: [ SpeciesId.SILVALLY ] } ] },
[BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MEWTWO, SpeciesId.MIRAIDON ] }
},
[BiomeId.END]: {
@ -6968,7 +6969,7 @@ export function initBiomes() {
]
],
[ SpeciesId.ETERNAL_FLOETTE, PokemonType.FAIRY, -1, [
[ BiomeId.FAIRY_CAVE, BiomePoolTier.RARE ],
[ BiomeId.FAIRY_CAVE, BiomePoolTier.SUPER_RARE ],
[ BiomeId.FAIRY_CAVE, BiomePoolTier.BOSS_RARE ]
]
],
@ -7707,10 +7708,10 @@ export function initBiomes() {
const traverseBiome = (biome: BiomeId, depth: number) => {
if (biome === BiomeId.END) {
const biomeList = Object.keys(BiomeId).filter(key => !Number.isNaN(Number(key)));
const biomeList = getEnumValues(BiomeId)
biomeList.pop(); // Removes BiomeId.END from the list
const randIndex = randSeedInt(biomeList.length, 1); // Will never be BiomeId.TOWN
biome = BiomeId[biomeList[randIndex]];
biome = biomeList[randIndex];
}
const linkedBiomes: (BiomeId | [ BiomeId, number ])[] = Array.isArray(biomeLinks[biome])
? biomeLinks[biome] as (BiomeId | [ BiomeId, number ])[]

View File

@ -1,8 +1,8 @@
import { allMoves } from "#data/data-lists";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { getEnumKeys, getEnumValues } from "#utils/common";
import { toReadableString } from "#utils/common";
import { getEnumKeys, getEnumValues } from "#utils/enums";
export const speciesEggMoves = {
[SpeciesId.BULBASAUR]: [ MoveId.SAPPY_SEED, MoveId.MALIGNANT_CHAIN, MoveId.EARTH_POWER, MoveId.MATCHA_GOTCHA ],
@ -584,17 +584,24 @@ export const speciesEggMoves = {
[SpeciesId.BLOODMOON_URSALUNA]: [ MoveId.NASTY_PLOT, MoveId.ROCK_POLISH, MoveId.SANDSEAR_STORM, MoveId.BOOMBURST ]
};
/**
* Parse a CSV-separated list of Egg Moves (such as one sourced from a Google Sheets)
* into code able to form the `speciesEggMoves` const object as above.
* @param content - The CSV-formatted string to convert into code.
*/
// TODO: Move this into the scripts folder and stop running it on initialization
function parseEggMoves(content: string): void {
let output = "";
const speciesNames = getEnumKeys(SpeciesId);
const speciesValues = getEnumValues(SpeciesId);
const moveNames = allMoves.map(m => m.name.replace(/ \([A-Z]\)$/, "").toLowerCase());
const lines = content.split(/\n/g);
for (const line of lines) {
const cols = line.split(",").slice(0, 5);
const moveNames = allMoves.map(m => m.name.replace(/ \([A-Z]\)$/, "").toLowerCase());
const enumSpeciesName = cols[0].toUpperCase().replace(/[ -]/g, "_");
const enumSpeciesName = cols[0].toUpperCase().replace(/[ -]/g, "_") as keyof typeof SpeciesId;
// TODO: This should use reverse mapping instead of `indexOf`
const species = speciesValues[speciesNames.indexOf(enumSpeciesName)];
const eggMoves: MoveId[] = [];
@ -602,14 +609,16 @@ function parseEggMoves(content: string): void {
for (let m = 0; m < 4; m++) {
const moveName = cols[m + 1].trim();
const moveIndex = moveName !== "N/A" ? moveNames.indexOf(moveName.toLowerCase()) : -1;
eggMoves.push(moveIndex > -1 ? moveIndex as MoveId : MoveId.NONE);
if (moveIndex === -1) {
console.warn(moveName, "could not be parsed");
}
eggMoves.push(moveIndex > -1 ? moveIndex as MoveId : MoveId.NONE);
}
if (eggMoves.find(m => m !== MoveId.NONE)) {
if (eggMoves.every(m => m === MoveId.NONE)) {
console.warn(`Species ${toReadableString(SpeciesId[species])} could not be parsed, excluding from output...`)
} else {
output += `[SpeciesId.${SpeciesId[species]}]: [ ${eggMoves.map(m => `MoveId.${MoveId[m]}`).join(", ")} ],\n`;
}
}

View File

@ -1630,7 +1630,7 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(SpeciesId.TSAREENA, 28, null, {key: EvoCondKey.MOVE, move: MoveId.STOMP}, SpeciesWildEvolutionDelay.LONG)
],
[SpeciesId.POIPOLE]: [
new SpeciesEvolution(SpeciesId.NAGANADEL, 1, null, {key: EvoCondKey.MOVE, move: MoveId.DRAGON_PULSE}, SpeciesWildEvolutionDelay.LONG)
new SpeciesEvolution(SpeciesId.NAGANADEL, 1, null, {key: EvoCondKey.MOVE, move: MoveId.DRAGON_PULSE}, SpeciesWildEvolutionDelay.VERY_LONG)
],
[SpeciesId.ALOLA_SANDSHREW]: [
new SpeciesEvolution(SpeciesId.ALOLA_SANDSLASH, 1, EvolutionItem.ICE_STONE, null, SpeciesWildEvolutionDelay.LONG)
@ -1845,7 +1845,7 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(SpeciesId.LEAVANNY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 120}, SpeciesWildEvolutionDelay.LONG)
],
[SpeciesId.TYPE_NULL]: [
new SpeciesEvolution(SpeciesId.SILVALLY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 100}, SpeciesWildEvolutionDelay.LONG)
new SpeciesEvolution(SpeciesId.SILVALLY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 100}, SpeciesWildEvolutionDelay.VERY_LONG)
],
[SpeciesId.ALOLA_MEOWTH]: [
new SpeciesEvolution(SpeciesId.ALOLA_PERSIAN, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 120}, SpeciesWildEvolutionDelay.LONG)

View File

@ -7,15 +7,8 @@ import { MoveFlags } from "#enums/MoveFlags";
import { AnimBlendType, AnimFocus, AnimFrameTarget, ChargeAnim, CommonAnim } from "#enums/move-anims-common";
import { MoveId } from "#enums/move-id";
import type { Pokemon } from "#field/pokemon";
import {
animationFileName,
coerceArray,
getEnumKeys,
getEnumValues,
getFrameMs,
isNullOrUndefined,
type nil,
} from "#utils/common";
import { animationFileName, coerceArray, getFrameMs, isNullOrUndefined, type nil } from "#utils/common";
import { getEnumKeys, getEnumValues } from "#utils/enums";
import Phaser from "phaser";
export class AnimConfig {
@ -1406,10 +1399,10 @@ export class EncounterBattleAnim extends BattleAnim {
export async function populateAnims() {
const commonAnimNames = getEnumKeys(CommonAnim).map(k => k.toLowerCase());
const commonAnimMatchNames = commonAnimNames.map(k => k.replace(/_/g, ""));
const commonAnimIds = getEnumValues(CommonAnim) as CommonAnim[];
const commonAnimIds = getEnumValues(CommonAnim);
const chargeAnimNames = getEnumKeys(ChargeAnim).map(k => k.toLowerCase());
const chargeAnimMatchNames = chargeAnimNames.map(k => k.replace(/_/g, " "));
const chargeAnimIds = getEnumValues(ChargeAnim) as ChargeAnim[];
const chargeAnimIds = getEnumValues(ChargeAnim);
const commonNamePattern = /name: (?:Common:)?(Opp )?(.*)/;
const moveNameToId = {};
for (const move of getEnumValues(MoveId).slice(1)) {

View File

@ -8,7 +8,8 @@ import { PartyMemberStrength } from "#enums/party-member-strength";
import type { SpeciesId } from "#enums/species-id";
import { PlayerPokemon } from "#field/pokemon";
import type { Starter } from "#ui/starter-select-ui-handler";
import { getEnumValues, randSeedGauss, randSeedInt, randSeedItem } from "#utils/common";
import { randSeedGauss, randSeedInt, randSeedItem } from "#utils/common";
import { getEnumValues } from "#utils/enums";
import { getPokemonSpecies } from "#utils/pokemon-utils";
export interface DailyRunConfig {
@ -165,5 +166,6 @@ export function getDailyStartingBiome(): BiomeId {
}
// Fallback in case something went wrong
// TODO: should this use `randSeedItem`?
return biomes[randSeedInt(biomes.length)];
}

View File

@ -1744,6 +1744,7 @@ export function getCharVariantFromDialogue(message: string): string {
}
export function initTrainerTypeDialogue(): void {
// TODO: this should not be using `Object.Keys`
const trainerTypes = Object.keys(trainerTypeDialogue).map(t => Number.parseInt(t) as TrainerType);
for (const trainerType of trainerTypes) {
const messages = trainerTypeDialogue[trainerType];

View File

@ -87,7 +87,8 @@ import type { AttackMoveResult } from "#types/attack-move-result";
import type { Localizable } from "#types/locales";
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString } from "#types/move-types";
import type { TurnMove } from "#types/turn-move";
import { BooleanHolder, coerceArray, type Constructor, getEnumValues, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue, toReadableString } from "#utils/common";
import { BooleanHolder, coerceArray, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue, toReadableString } from "#utils/common";
import { getEnumValues } from "#utils/enums";
import i18next from "i18next";
/**

View File

@ -36,7 +36,8 @@ import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
import i18next from "#plugins/i18n";
import { PokemonData } from "#system/pokemon-data";
import { randSeedInt } from "#utils/common";
import { randSeedItem } from "#utils/common";
import { getEnumValues } from "#utils/enums";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/berriesAbound";
@ -310,7 +311,7 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder.
.build();
function tryGiveBerry(prioritizedPokemon?: PlayerPokemon) {
const berryType = randSeedInt(Object.keys(BerryType).filter(s => !Number.isNaN(Number(s))).length) as BerryType;
const berryType = randSeedItem(getEnumValues(BerryType));
const berry = generateModifierType(modifierTypes.BERRY, [berryType]) as BerryModifierType;
const party = globalScene.getPlayerParty();

View File

@ -44,8 +44,8 @@ import {
HeldItemRequirement,
TypeRequirement,
} from "#mystery-encounters/mystery-encounter-requirements";
import { TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#trainers/TrainerPartyTemplate";
import { getRandomPartyMemberFunc, trainerConfigs } from "#trainers/trainer-config";
import { TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#trainers/trainer-party-template";
import type { OptionSelectItem } from "#ui/abstact-option-select-ui-handler";
import { MoveInfoOverlay } from "#ui/move-info-overlay";
import { isNullOrUndefined, randSeedInt, randSeedShuffle } from "#utils/common";
@ -289,6 +289,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
// Init the moves available for tutor
const moveTutorOptions: PokemonMove[] = [];
// TODO: should this use `randSeedItem`?
moveTutorOptions.push(new PokemonMove(PHYSICAL_TUTOR_MOVES[randSeedInt(PHYSICAL_TUTOR_MOVES.length)]));
moveTutorOptions.push(new PokemonMove(SPECIAL_TUTOR_MOVES[randSeedInt(SPECIAL_TUTOR_MOVES.length)]));
moveTutorOptions.push(new PokemonMove(STATUS_TUTOR_MOVES[randSeedInt(STATUS_TUTOR_MOVES.length)]));
@ -386,6 +387,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
specialOptions.push(rareFormChangeModifier);
}
if (specialOptions.length > 0) {
// TODO: should this use `randSeedItem`?
modifierOptions.push(specialOptions[randSeedInt(specialOptions.length)]);
}

View File

@ -43,8 +43,8 @@ import {
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
import { TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#trainers/TrainerPartyTemplate";
import { trainerConfigs } from "#trainers/trainer-config";
import { TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#trainers/trainer-party-template";
import type { OptionSelectConfig } from "#ui/abstact-option-select-ui-handler";
import { randSeedInt, randSeedShuffle } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
@ -138,6 +138,7 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder
clownConfig.partyTemplateFunc = null; // Overrides party template func if it exists
// Generate random ability for Blacephalon from pool
// TODO: should this use `randSeedItem`?
const ability = RANDOM_ABILITY_POOL[randSeedInt(RANDOM_ABILITY_POOL.length)];
encounter.setDialogueToken("ability", allAbilities[ability].name);
encounter.misc = { ability };

View File

@ -9,12 +9,12 @@ import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils
import { initBattleWithEnemyConfig, setEncounterRewards } from "#mystery-encounters/encounter-phase-utils";
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { trainerConfigs } from "#trainers/trainer-config";
import {
TrainerPartyCompoundTemplate,
TrainerPartyTemplate,
trainerPartyTemplates,
} from "#trainers/TrainerPartyTemplate";
import { trainerConfigs } from "#trainers/trainer-config";
} from "#trainers/trainer-party-template";
import { randSeedInt } from "#utils/common";
/** the i18n namespace for the encounter */

View File

@ -190,6 +190,7 @@ async function doBiomeTransitionDialogueAndBattleInit() {
// Calculate new biome (cannot be current biome)
const filteredBiomes = BIOME_CANDIDATES.filter(b => globalScene.arena.biomeType !== b);
// TODO: should this use `randSeedItem`?
const newBiome = filteredBiomes[randSeedInt(filteredBiomes.length)];
// Show dialogue and transition biome

View File

@ -28,7 +28,8 @@ import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encou
import { PokemonData } from "#system/pokemon-data";
import type { HeldModifierConfig } from "#types/held-modifier-config";
import type { OptionSelectItem } from "#ui/abstact-option-select-ui-handler";
import { getEnumValues, isNullOrUndefined, randSeedShuffle } from "#utils/common";
import { isNullOrUndefined, randSeedShuffle } from "#utils/common";
import { getEnumValues } from "#utils/enums";
import i18next from "i18next";
/** The i18n namespace for the encounter */

View File

@ -36,8 +36,8 @@ import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encou
import i18next from "#plugins/i18n";
import { achvs } from "#system/achv";
import { PokemonData } from "#system/pokemon-data";
import { TrainerPartyTemplate } from "#trainers/TrainerPartyTemplate";
import { trainerConfigs } from "#trainers/trainer-config";
import { TrainerPartyTemplate } from "#trainers/trainer-party-template";
import type { HeldModifierConfig } from "#types/held-modifier-config";
import { isNullOrUndefined, NumberHolder, randSeedInt, randSeedShuffle } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
@ -305,6 +305,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
// One random pokemon will get its passive unlocked
const passiveDisabledPokemon = globalScene.getPlayerParty().filter(p => !p.passive);
if (passiveDisabledPokemon?.length > 0) {
// TODO: should this use `randSeedItem`?
const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)];
enablePassiveMon.passive = true;
enablePassiveMon.updateInfo(true);
@ -466,6 +467,7 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
// One random pokemon will get its passive unlocked
const passiveDisabledPokemon = globalScene.getPlayerParty().filter(p => !p.passive);
if (passiveDisabledPokemon?.length > 0) {
// TODO: should this use `randSeedItem`?
const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)];
enablePassiveMon.passive = true;
await enablePassiveMon.updateInfo(true);

View File

@ -144,11 +144,13 @@ export class MysteryEncounterOption implements IMysteryEncounterOption {
}
if (truePrimaryPool.length > 0) {
// always choose from the non-overlapping pokemon first
// TODO: should this use `randSeedItem`?
this.primaryPokemon = truePrimaryPool[randSeedInt(truePrimaryPool.length)];
return true;
}
// if there are multiple overlapping pokemon, we're okay - just choose one and take it out of the supporting pokemon pool
if (overlap.length > 1 || this.secondaryPokemon.length - overlap.length >= 1) {
// TODO: should this use `randSeedItem`?
this.primaryPokemon = overlap[randSeedInt(overlap.length)];
this.secondaryPokemon = this.secondaryPokemon.filter(supp => supp !== this.primaryPokemon);
return true;

View File

@ -381,6 +381,7 @@ export class MysteryEncounter implements IMysteryEncounter {
// If there are multiple overlapping pokemon, we're okay - just choose one and take it out of the primary pokemon pool
if (overlap.length > 1 || this.secondaryPokemon.length - overlap.length >= 1) {
// is this working?
// TODO: should this use `randSeedItem`?
this.primaryPokemon = overlap[randSeedInt(overlap.length, 0)];
this.secondaryPokemon = this.secondaryPokemon.filter(supp => supp !== this.primaryPokemon);
return true;
@ -391,6 +392,7 @@ export class MysteryEncounter implements IMysteryEncounter {
return false;
}
// this means we CAN have the same pokemon be a primary and secondary pokemon, so just choose any qualifying one randomly.
// TODO: should this use `randSeedItem`?
this.primaryPokemon = qualified[randSeedInt(qualified.length, 0)];
return true;
}

View File

@ -1098,8 +1098,10 @@ export function calculateMEAggregateStats(baseSpawnWeight: number) {
if (biomes! && biomes.length > 0) {
const specialBiomes = biomes.filter(b => alwaysPickTheseBiomes.includes(b));
if (specialBiomes.length > 0) {
// TODO: should this use `randSeedItem`?
currentBiome = specialBiomes[randSeedInt(specialBiomes.length)];
} else {
// TODO: should this use `randSeedItem`?
currentBiome = biomes[randSeedInt(biomes.length)];
}
}

View File

@ -110,20 +110,24 @@ export function getRandomPlayerPokemon(
// If there is only 1 legal/unfainted mon left, select from fainted legal mons
const faintedLegalMons = party.filter(p => (!isAllowed || p.isAllowedInChallenge()) && p.isFainted());
if (faintedLegalMons.length > 0) {
// TODO: should this use `randSeedItem`?
chosenIndex = randSeedInt(faintedLegalMons.length);
chosenPokemon = faintedLegalMons[chosenIndex];
}
}
if (!chosenPokemon && fullyLegalMons.length > 0) {
// TODO: should this use `randSeedItem`?
chosenIndex = randSeedInt(fullyLegalMons.length);
chosenPokemon = fullyLegalMons[chosenIndex];
}
if (!chosenPokemon && isAllowed && allowedOnlyMons.length > 0) {
// TODO: should this use `randSeedItem`?
chosenIndex = randSeedInt(allowedOnlyMons.length);
chosenPokemon = allowedOnlyMons[chosenIndex];
}
if (!chosenPokemon) {
// If no other options worked, returns fully random
// TODO: should this use `randSeedItem`?
chosenIndex = randSeedInt(party.length);
chosenPokemon = party[chosenIndex];
}
@ -517,7 +521,7 @@ export function trainerThrowPokeball(
repeatDelay: 500,
onUpdate: t => {
if (shakeCount && shakeCount < 4) {
const value = t.getValue();
const value = t.getValue() ?? 0;
const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1;
pokeball.setX(pbX + value * 4 * directionMultiplier);
pokeball.setAngle(value * 27.5 * directionMultiplier);

View File

@ -127,7 +127,7 @@ export function doPokemonTransformationSequence(
to: 1,
duration: 1000,
onUpdate: t => {
pokemonTintSprite.setAlpha(t.getValue());
pokemonTintSprite.setAlpha(t.getValue() ?? 1);
},
onComplete: () => {
pokemonSprite.setVisible(false);

View File

@ -0,0 +1,376 @@
import { FixedBattleConfig, getRandomTrainerFunc } from "#app/battle";
import { Trainer } from "#app/field/trainer";
import { globalScene } from "#app/global-scene";
import { randSeedInt } from "#app/utils/common";
import { BattleType } from "#enums/battle-type";
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
import { ModifierTier } from "#enums/modifier-tier";
import { PlayerGender } from "#enums/player-gender";
import { TrainerType } from "#enums/trainer-type";
import { TrainerVariant } from "#enums/trainer-variant";
export interface FixedBattleConfigs {
[key: number]: FixedBattleConfig;
}
/**
* Youngster/Lass on 5
* Rival on 8, 55, 95, 145, 195
* Evil team grunts on 35, 62, 64, and 112
* Evil team admin on 66 and 114
* Evil leader on 115, 165
* E4 on 182, 184, 186, 188
* Champion on 190
*/
export const classicFixedBattles: FixedBattleConfigs = {
[ClassicFixedBossWaves.TOWN_YOUNGSTER]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
() => new Trainer(TrainerType.YOUNGSTER, randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT),
),
[ClassicFixedBossWaves.RIVAL_1]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
() =>
new Trainer(
TrainerType.RIVAL,
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
),
),
[ClassicFixedBossWaves.RIVAL_2]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
() =>
new Trainer(
TrainerType.RIVAL_2,
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
),
)
.setCustomModifierRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT],
allowLuckUpgrades: false,
}),
[ClassicFixedBossWaves.EVIL_GRUNT_1]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
getRandomTrainerFunc(
[
TrainerType.ROCKET_GRUNT,
TrainerType.MAGMA_GRUNT,
TrainerType.AQUA_GRUNT,
TrainerType.GALACTIC_GRUNT,
TrainerType.PLASMA_GRUNT,
TrainerType.FLARE_GRUNT,
TrainerType.AETHER_GRUNT,
TrainerType.SKULL_GRUNT,
TrainerType.MACRO_GRUNT,
TrainerType.STAR_GRUNT,
],
true,
),
),
[ClassicFixedBossWaves.RIVAL_3]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
() =>
new Trainer(
TrainerType.RIVAL_3,
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
),
)
.setCustomModifierRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT],
allowLuckUpgrades: false,
}),
[ClassicFixedBossWaves.EVIL_GRUNT_2]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(
getRandomTrainerFunc(
[
TrainerType.ROCKET_GRUNT,
TrainerType.MAGMA_GRUNT,
TrainerType.AQUA_GRUNT,
TrainerType.GALACTIC_GRUNT,
TrainerType.PLASMA_GRUNT,
TrainerType.FLARE_GRUNT,
TrainerType.AETHER_GRUNT,
TrainerType.SKULL_GRUNT,
TrainerType.MACRO_GRUNT,
TrainerType.STAR_GRUNT,
],
true,
),
),
[ClassicFixedBossWaves.EVIL_GRUNT_3]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(
getRandomTrainerFunc(
[
TrainerType.ROCKET_GRUNT,
TrainerType.MAGMA_GRUNT,
TrainerType.AQUA_GRUNT,
TrainerType.GALACTIC_GRUNT,
TrainerType.PLASMA_GRUNT,
TrainerType.FLARE_GRUNT,
TrainerType.AETHER_GRUNT,
TrainerType.SKULL_GRUNT,
TrainerType.MACRO_GRUNT,
TrainerType.STAR_GRUNT,
],
true,
),
),
[ClassicFixedBossWaves.EVIL_ADMIN_1]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(
getRandomTrainerFunc(
[
[TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL],
[TrainerType.TABITHA, TrainerType.COURTNEY],
[TrainerType.MATT, TrainerType.SHELLY],
[TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN],
[TrainerType.ZINZOLIN, TrainerType.COLRESS],
[TrainerType.XEROSIC, TrainerType.BRYONY],
TrainerType.FABA,
TrainerType.PLUMERIA,
TrainerType.OLEANA,
[TrainerType.GIACOMO, TrainerType.MELA, TrainerType.ATTICUS, TrainerType.ORTEGA, TrainerType.ERI],
],
true,
),
),
[ClassicFixedBossWaves.RIVAL_4]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
() =>
new Trainer(
TrainerType.RIVAL_4,
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
),
)
.setCustomModifierRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA],
allowLuckUpgrades: false,
}),
[ClassicFixedBossWaves.EVIL_GRUNT_4]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(
getRandomTrainerFunc(
[
TrainerType.ROCKET_GRUNT,
TrainerType.MAGMA_GRUNT,
TrainerType.AQUA_GRUNT,
TrainerType.GALACTIC_GRUNT,
TrainerType.PLASMA_GRUNT,
TrainerType.FLARE_GRUNT,
TrainerType.AETHER_GRUNT,
TrainerType.SKULL_GRUNT,
TrainerType.MACRO_GRUNT,
TrainerType.STAR_GRUNT,
],
true,
),
),
[ClassicFixedBossWaves.EVIL_ADMIN_2]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(
getRandomTrainerFunc(
[
[TrainerType.ARCHER, TrainerType.ARIANA, TrainerType.PROTON, TrainerType.PETREL],
[TrainerType.TABITHA, TrainerType.COURTNEY],
[TrainerType.MATT, TrainerType.SHELLY],
[TrainerType.JUPITER, TrainerType.MARS, TrainerType.SATURN],
[TrainerType.ZINZOLIN, TrainerType.COLRESS],
[TrainerType.XEROSIC, TrainerType.BRYONY],
TrainerType.FABA,
TrainerType.PLUMERIA,
TrainerType.OLEANA,
[TrainerType.GIACOMO, TrainerType.MELA, TrainerType.ATTICUS, TrainerType.ORTEGA, TrainerType.ERI],
],
true,
1,
),
),
[ClassicFixedBossWaves.EVIL_BOSS_1]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(
getRandomTrainerFunc([
TrainerType.ROCKET_BOSS_GIOVANNI_1,
TrainerType.MAXIE,
TrainerType.ARCHIE,
TrainerType.CYRUS,
TrainerType.GHETSIS,
TrainerType.LYSANDRE,
TrainerType.LUSAMINE,
TrainerType.GUZMA,
TrainerType.ROSE,
TrainerType.PENNY,
]),
)
.setCustomModifierRewards({
guaranteedModifierTiers: [
ModifierTier.ROGUE,
ModifierTier.ROGUE,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
],
allowLuckUpgrades: false,
}),
[ClassicFixedBossWaves.RIVAL_5]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
() =>
new Trainer(
TrainerType.RIVAL_5,
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
),
)
.setCustomModifierRewards({
guaranteedModifierTiers: [
ModifierTier.ROGUE,
ModifierTier.ROGUE,
ModifierTier.ROGUE,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
],
allowLuckUpgrades: false,
}),
[ClassicFixedBossWaves.EVIL_BOSS_2]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(
getRandomTrainerFunc([
TrainerType.ROCKET_BOSS_GIOVANNI_2,
TrainerType.MAXIE_2,
TrainerType.ARCHIE_2,
TrainerType.CYRUS_2,
TrainerType.GHETSIS_2,
TrainerType.LYSANDRE_2,
TrainerType.LUSAMINE_2,
TrainerType.GUZMA_2,
TrainerType.ROSE_2,
TrainerType.PENNY_2,
]),
)
.setCustomModifierRewards({
guaranteedModifierTiers: [
ModifierTier.ROGUE,
ModifierTier.ROGUE,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
],
allowLuckUpgrades: false,
}),
[ClassicFixedBossWaves.ELITE_FOUR_1]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
getRandomTrainerFunc([
TrainerType.LORELEI,
TrainerType.WILL,
TrainerType.SIDNEY,
TrainerType.AARON,
TrainerType.SHAUNTAL,
TrainerType.MALVA,
[TrainerType.HALA, TrainerType.MOLAYNE],
TrainerType.MARNIE_ELITE,
TrainerType.RIKA,
TrainerType.CRISPIN,
]),
),
[ClassicFixedBossWaves.ELITE_FOUR_2]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.ELITE_FOUR_1)
.setGetTrainerFunc(
getRandomTrainerFunc([
TrainerType.BRUNO,
TrainerType.KOGA,
TrainerType.PHOEBE,
TrainerType.BERTHA,
TrainerType.MARSHAL,
TrainerType.SIEBOLD,
TrainerType.OLIVIA,
TrainerType.NESSA_ELITE,
TrainerType.POPPY,
TrainerType.AMARYS,
]),
),
[ClassicFixedBossWaves.ELITE_FOUR_3]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.ELITE_FOUR_1)
.setGetTrainerFunc(
getRandomTrainerFunc([
TrainerType.AGATHA,
TrainerType.BRUNO,
TrainerType.GLACIA,
TrainerType.FLINT,
TrainerType.GRIMSLEY,
TrainerType.WIKSTROM,
TrainerType.ACEROLA,
[TrainerType.BEA_ELITE, TrainerType.ALLISTER_ELITE],
TrainerType.LARRY_ELITE,
TrainerType.LACEY,
]),
),
[ClassicFixedBossWaves.ELITE_FOUR_4]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.ELITE_FOUR_1)
.setGetTrainerFunc(
getRandomTrainerFunc([
TrainerType.LANCE,
TrainerType.KAREN,
TrainerType.DRAKE,
TrainerType.LUCIAN,
TrainerType.CAITLIN,
TrainerType.DRASNA,
TrainerType.KAHILI,
TrainerType.RAIHAN_ELITE,
TrainerType.HASSEL,
TrainerType.DRAYTON,
]),
),
[ClassicFixedBossWaves.CHAMPION]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.ELITE_FOUR_1)
.setGetTrainerFunc(
getRandomTrainerFunc([
TrainerType.BLUE,
[TrainerType.RED, TrainerType.LANCE_CHAMPION],
[TrainerType.STEVEN, TrainerType.WALLACE],
TrainerType.CYNTHIA,
[TrainerType.ALDER, TrainerType.IRIS],
TrainerType.DIANTHA,
[TrainerType.KUKUI, TrainerType.HAU],
[TrainerType.LEON, TrainerType.MUSTARD],
[TrainerType.GEETA, TrainerType.NEMONA],
TrainerType.KIERAN,
]),
),
[ClassicFixedBossWaves.RIVAL_6]: new FixedBattleConfig()
.setBattleType(BattleType.TRAINER)
.setGetTrainerFunc(
() =>
new Trainer(
TrainerType.RIVAL_6,
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
),
)
.setCustomModifierRewards({
guaranteedModifierTiers: [
ModifierTier.ROGUE,
ModifierTier.ROGUE,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
ModifierTier.GREAT,
ModifierTier.GREAT,
],
allowLuckUpgrades: false,
}),
};

View File

@ -30,7 +30,7 @@ import {
TrainerPartyCompoundTemplate,
TrainerPartyTemplate,
trainerPartyTemplates,
} from "#trainers/TrainerPartyTemplate";
} from "#trainers/trainer-party-template";
import type { ModifierTypeFunc } from "#types/modifier-types";
import type {
GenAIFunc,

View File

@ -143,6 +143,7 @@ export class Arena {
if (!tierPool.length) {
ret = globalScene.randomSpecies(waveIndex, level);
} else {
// TODO: should this use `randSeedItem`?
const entry = tierPool[randSeedInt(tierPool.length)];
let species: SpeciesId;
if (typeof entry === "number") {
@ -154,6 +155,7 @@ export class Arena {
if (level >= levelThreshold) {
const speciesIds = entry[levelThreshold];
if (speciesIds.length > 1) {
// TODO: should this use `randSeedItem`?
species = speciesIds[randSeedInt(speciesIds.length)];
} else {
species = speciesIds[0];
@ -166,19 +168,11 @@ export class Arena {
ret = getPokemonSpecies(species!);
if (ret.subLegendary || ret.legendary || ret.mythical) {
switch (true) {
case ret.baseTotal >= 720:
regen = level < 90;
break;
case ret.baseTotal >= 670:
regen = level < 70;
break;
case ret.baseTotal >= 580:
regen = level < 50;
break;
default:
regen = level < 30;
break;
const waveDifficulty = globalScene.gameMode.getWaveForDifficulty(waveIndex);
if (ret.baseTotal >= 660) {
regen = waveDifficulty < 80; // Wave 50+ in daily (however, max Daily wave is 50 currently so not possible)
} else {
regen = waveDifficulty < 55; // Wave 25+ in daily
}
}
}
@ -496,38 +490,37 @@ export class Arena {
getTrainerChance(): number {
switch (this.biomeType) {
case BiomeId.METROPOLIS:
return 2;
case BiomeId.SLUM:
case BiomeId.BEACH:
case BiomeId.DOJO:
case BiomeId.CONSTRUCTION_SITE:
return 4;
case BiomeId.PLAINS:
case BiomeId.GRASS:
case BiomeId.BEACH:
case BiomeId.LAKE:
case BiomeId.CAVE:
case BiomeId.DESERT:
case BiomeId.CONSTRUCTION_SITE:
case BiomeId.SLUM:
return 6;
case BiomeId.TALL_GRASS:
case BiomeId.FOREST:
case BiomeId.SEA:
case BiomeId.SWAMP:
case BiomeId.MOUNTAIN:
case BiomeId.BADLANDS:
case BiomeId.DESERT:
case BiomeId.MEADOW:
case BiomeId.POWER_PLANT:
case BiomeId.GRAVEYARD:
case BiomeId.FACTORY:
case BiomeId.SNOWY_FOREST:
return 8;
case BiomeId.SEA:
case BiomeId.ICE_CAVE:
case BiomeId.VOLCANO:
case BiomeId.GRAVEYARD:
case BiomeId.RUINS:
case BiomeId.WASTELAND:
case BiomeId.JUNGLE:
case BiomeId.FAIRY_CAVE:
case BiomeId.ISLAND:
return 12;
case BiomeId.SEABED:
case BiomeId.ABYSS:
case BiomeId.SPACE:
case BiomeId.TEMPLE:

View File

@ -155,7 +155,6 @@ import {
coerceArray,
deltaRgb,
fixedInt,
getEnumValues,
getIvsFromId,
isBetween,
isNullOrUndefined,
@ -169,6 +168,7 @@ import {
rgbToHsv,
toDmgValue,
} from "#utils/common";
import { getEnumValues } from "#utils/enums";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities";
import i18next from "i18next";
@ -2525,41 +2525,50 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* @returns A score value based on how favorable this Pokemon is when fighting the given Pokemon
*/
getMatchupScore(opponent: Pokemon): number {
const types = this.getTypes(true);
const enemyTypes = opponent.getTypes(true, true, false, true);
const enemyTypes = opponent.getTypes(true, false, false, true);
/** Is this Pokemon faster than the opponent? */
const outspeed =
(this.isActive(true) ? this.getEffectiveStat(Stat.SPD, opponent) : this.getStat(Stat.SPD, false)) >=
opponent.getEffectiveStat(Stat.SPD, this);
/**
* Based on how effective this Pokemon's types are offensively against the opponent's types.
* This score is increased by 25 percent if this Pokemon is faster than the opponent.
*/
let atkScore =
opponent.getAttackTypeEffectiveness(types[0], this, false, true, undefined, true) * (outspeed ? 1.25 : 1);
/**
* Based on how effectively this Pokemon defends against the opponent's types.
* This score cannot be higher than 4.
*/
let defScore = 1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[0], opponent), 0.25);
if (types.length > 1) {
atkScore *= opponent.getAttackTypeEffectiveness(types[1], this);
}
if (enemyTypes.length > 1) {
defScore *=
1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[1], opponent, false, false, undefined, true), 0.25);
}
atkScore *= 1.25; //give more value for the pokemon's typing
const moveset = this.moveset;
let moveAtkScoreLength = 0;
let atkScore = 0;
// TODO: this calculation needs to consider more factors; it's currently very simplistic
for (const move of moveset) {
if (move.getMove().category === MoveCategory.SPECIAL || move.getMove().category === MoveCategory.PHYSICAL) {
atkScore += opponent.getAttackTypeEffectiveness(move.getMove().type, this, false, true, undefined, true);
moveAtkScoreLength++;
const resolvedMove = move.getMove();
// NOTE: Counter and Mirror Coat are considered as attack moves here
if (resolvedMove.category === MoveCategory.STATUS || move.getPpRatio() <= 0) {
continue;
}
const moveType = resolvedMove.type;
let thisScore = opponent.getAttackTypeEffectiveness(moveType, this, false, true, undefined, true);
// Add STAB multiplier for attack type effectiveness.
// For now, simply don't apply STAB to moves that may change type
if (this.getTypes(true).includes(moveType) && !move.getMove().hasAttr("VariableMoveTypeAttr")) {
thisScore *= 1.5;
}
atkScore += thisScore;
moveAtkScoreLength++;
}
atkScore = atkScore / (moveAtkScoreLength + 1); //calculate the median for the attack score
// Get average attack score of all damaging moves (|| 1 prevents division by zero))
// TODO: Averaging the attack score is excessively simplistic, and doesn't reflect the AI's move selection logic
// e.g. if the mon has one 4x effective move and three 0.5x effective moves, this score would be ~1.375
// which does not seem fair, given that if the AI were to switch, in all likelihood it would use the 4x move.
// We could consider a weighted average...
atkScore /= moveAtkScoreLength || 1;
/**
* Based on this Pokemon's HP ratio compared to that of the opponent.
* This ratio is multiplied by 1.5 if this Pokemon outspeeds the opponent;
@ -2567,6 +2576,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
*/
const hpRatio = this.getHpRatio();
const oppHpRatio = opponent.getHpRatio();
// TODO: use better logic for predicting whether the pokemon "is dying"
// E.g., perhaps check if it would faint if the opponent were to use the same move it just used
// (twice if the user is slower)
const isDying = hpRatio <= 0.2;
let hpDiffRatio = hpRatio + (1 - oppHpRatio);
if (isDying && this.isActive(true)) {
@ -2576,15 +2588,15 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
//It might not be a worthy sacrifice if it doesn't outspeed or doesn't do enough damage
hpDiffRatio *= 0.85;
} else {
hpDiffRatio = Math.min(1 - hpRatio + (outspeed ? 0.2 : 0.1), 1);
hpDiffRatio = 1 - hpRatio + (outspeed ? 0.2 : 0.1);
}
} else if (outspeed) {
hpDiffRatio = Math.min(hpDiffRatio * 1.25, 1);
hpDiffRatio = hpDiffRatio * 1.25;
} else if (hpRatio > 0.2 && hpRatio <= 0.4) {
//Might be considered to be switched because it's not in low enough health
hpDiffRatio = Math.min(hpDiffRatio * 0.5, 1);
// Might be considered to be switched because it's not in low enough health
hpDiffRatio = hpDiffRatio * 0.5;
}
return (atkScore + defScore) * hpDiffRatio;
return (atkScore + defScore) * Math.min(hpDiffRatio, 1);
}
getEvolution(): SpeciesFormEvolution | null {

View File

@ -14,10 +14,13 @@ import { TrainerVariant } from "#enums/trainer-variant";
import type { EnemyPokemon } from "#field/pokemon";
import type { PersistentModifier } from "#modifiers/modifier";
import { getIsInitialized, initI18n } from "#plugins/i18n";
import type { TrainerPartyTemplate } from "#trainers/TrainerPartyTemplate";
import { TrainerPartyCompoundTemplate, trainerPartyTemplates } from "#trainers/TrainerPartyTemplate";
import type { TrainerConfig } from "#trainers/trainer-config";
import { trainerConfigs } from "#trainers/trainer-config";
import {
TrainerPartyCompoundTemplate,
type TrainerPartyTemplate,
trainerPartyTemplates,
} from "#trainers/trainer-party-template";
import { randSeedInt, randSeedItem, randSeedWeightedItem } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import i18next from "i18next";
@ -420,7 +423,8 @@ export class Trainer extends Phaser.GameObjects.Container {
// If useNewSpeciesPool is true, we need to generate a new species from the new species pool, otherwise we generate a random species
let species = useNewSpeciesPool
? getPokemonSpecies(newSpeciesPool[Math.floor(randSeedInt(newSpeciesPool.length))])
? // TODO: should this use `randSeedItem`?
getPokemonSpecies(newSpeciesPool[Math.floor(randSeedInt(newSpeciesPool.length))])
: template.isSameSpecies(index) && index > offset
? getPokemonSpecies(
battle.enemyParty[offset].species.getTrainerSpeciesForLevel(
@ -618,6 +622,8 @@ export class Trainer extends Phaser.GameObjects.Container {
if (maxScorePartyMemberIndexes.length > 1) {
let rand: number;
// TODO: should this use `randSeedItem`?
globalScene.executeWithSeedOffset(
() => (rand = randSeedInt(maxScorePartyMemberIndexes.length)),
globalScene.currentBattle.turn << 2,

View File

@ -1,5 +1,4 @@
import type { FixedBattleConfigs } from "#app/battle";
import { classicFixedBattles, FixedBattleConfig } from "#app/battle";
import { FixedBattleConfig } from "#app/battle";
import { CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene";
import Overrides from "#app/overrides";
@ -14,6 +13,7 @@ import { Challenges } from "#enums/challenges";
import { GameModes } from "#enums/game-modes";
import { SpeciesId } from "#enums/species-id";
import type { Arena } from "#field/arena";
import { classicFixedBattles, type FixedBattleConfigs } from "#trainers/fixed-battle-configs";
import { isNullOrUndefined, randSeedInt, randSeedItem } from "#utils/common";
import i18next from "i18next";
@ -164,14 +164,14 @@ export class GameMode implements GameModeConfig {
if (waveIndex % 10 !== 1 && waveIndex % 10) {
/**
* Do not check X1 floors since there's a bug that stops trainer sprites from appearing
* after a X0 full party heal
* after a X0 full party heal, this also allows for a smoother biome transition for general gameplay feel
*/
const trainerChance = arena.getTrainerChance();
let allowTrainerBattle = true;
if (trainerChance) {
const waveBase = Math.floor(waveIndex / 10) * 10;
// Stop generic trainers from spawning in within 3 waves of a trainer battle
for (let w = Math.max(waveIndex - 3, waveBase + 2); w <= Math.min(waveIndex + 3, waveBase + 9); w++) {
// Stop generic trainers from spawning in within 2 waves of a fixed trainer battle
for (let w = Math.max(waveIndex - 2, waveBase + 2); w <= Math.min(waveIndex + 2, waveBase + 9); w++) {
if (w === waveIndex) {
continue;
}

View File

@ -15,8 +15,8 @@ import type { SettingKeyboard } from "#system/settings-keyboard";
import { MoveTouchControlsHandler } from "#ui/move-touch-controls-handler";
import type { SettingsGamepadUiHandler } from "#ui/settings-gamepad-ui-handler";
import type { SettingsKeyboardUiHandler } from "#ui/settings-keyboard-ui-handler";
import { getEnumValues } from "#utils/common";
import { deepCopy } from "#utils/data";
import { getEnumValues } from "#utils/enums";
import Phaser from "phaser";
export interface DeviceMapping {

View File

@ -21,7 +21,8 @@ import { initAchievements } from "#system/achv";
import { initVouchers } from "#system/voucher";
import { initStatsKeys } from "#ui/game-stats-ui-handler";
import { getWindowVariantSuffix, WindowVariant } from "#ui/ui-theme";
import { getEnumValues, hasAllLocalizedSprites, localPing } from "#utils/common";
import { hasAllLocalizedSprites, localPing } from "#utils/common";
import { getEnumValues } from "#utils/enums";
import i18next from "i18next";
export class LoadingScene extends SceneBase {

View File

@ -116,15 +116,8 @@ import type { ModifierTypeFunc, WeightedModifierTypeWeightFunc } from "#types/mo
import type { PokemonMoveSelectFilter, PokemonSelectFilter } from "#ui/party-ui-handler";
import { PartyUiHandler } from "#ui/party-ui-handler";
import { getModifierTierTextTint } from "#ui/text";
import {
formatMoney,
getEnumKeys,
getEnumValues,
isNullOrUndefined,
NumberHolder,
padInt,
randSeedInt,
} from "#utils/common";
import { formatMoney, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#utils/common";
import { getEnumKeys, getEnumValues } from "#utils/enums";
import { getModifierPoolForType, getModifierType } from "#utils/modifier-utils";
import i18next from "i18next";
@ -1524,6 +1517,7 @@ class TmModifierTypeGenerator extends ModifierTypeGenerator {
if (!tierUniqueCompatibleTms.length) {
return null;
}
// TODO: should this use `randSeedItem`?
const randTmIndex = randSeedInt(tierUniqueCompatibleTms.length);
return new TmModifierType(tierUniqueCompatibleTms[randTmIndex]);
});
@ -1577,6 +1571,7 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator {
return null;
}
// TODO: should this use `randSeedItem`?
return new EvolutionItemModifierType(evolutionItemPool[randSeedInt(evolutionItemPool.length)]!); // TODO: is the bang correct?
});
}
@ -1662,6 +1657,7 @@ export class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator {
return null;
}
// TODO: should this use `randSeedItem`?
return new FormChangeItemModifierType(formChangeItemPool[randSeedInt(formChangeItemPool.length)]);
});
}
@ -1932,7 +1928,7 @@ const modifierTypeInitObj = Object.freeze({
if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in Nature) {
return new PokemonNatureChangeModifierType(pregenArgs[0] as Nature);
}
return new PokemonNatureChangeModifierType(randSeedInt(getEnumValues(Nature).length) as Nature);
return new PokemonNatureChangeModifierType(randSeedItem(getEnumValues(Nature)));
}),
MYSTICAL_ROCK: () =>

View File

@ -120,7 +120,7 @@ export class AttemptCapturePhase extends PokemonPhase {
repeatDelay: 500,
onUpdate: t => {
if (shakeCount && shakeCount < (isCritical ? 2 : 4)) {
const value = t.getValue();
const value = t.getValue() ?? 0;
const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1;
this.pokeball.setX(pbX + value * 4 * directionMultiplier);
this.pokeball.setAngle(value * 27.5 * directionMultiplier);

View File

@ -239,7 +239,7 @@ export class EvolutionPhase extends Phase {
to: 1,
duration: 2000,
onUpdate: t => {
this.pokemonTintSprite.setAlpha(t.getValue());
this.pokemonTintSprite.setAlpha(t.getValue() ?? 1);
},
onComplete: () => {
this.pokemonSprite.setVisible(false);

View File

@ -26,7 +26,8 @@ import { applyMoveAttrs } from "#moves/apply-attrs";
import { frenzyMissFunc } from "#moves/move-utils";
import type { PokemonMove } from "#moves/pokemon-move";
import { BattlePhase } from "#phases/battle-phase";
import { enumValueToKey, NumberHolder } from "#utils/common";
import { NumberHolder } from "#utils/common";
import { enumValueToKey } from "#utils/enums";
import i18next from "i18next";
export class MovePhase extends BattlePhase {

View File

@ -56,6 +56,7 @@ export class SelectBiomePhase extends BattlePhase {
delay: 1000,
});
} else {
// TODO: should this use `randSeedItem`?
setNextBiome(biomes[randSeedInt(biomes.length)]);
}
} else if (biomeLinks.hasOwnProperty(currentBiome)) {

View File

@ -179,7 +179,7 @@ export class SelectModifierPhase extends BattlePhase {
} else {
this.applyModifier(modifierType.newModifier()!);
}
return !cost;
return cost === -1;
}
// Reroll rewards

View File

@ -62,8 +62,9 @@ import { VoucherType, vouchers } from "#system/voucher";
import { trainerConfigs } from "#trainers/trainer-config";
import type { DexData, DexEntry } from "#types/dex-data";
import { RUN_HISTORY_LIMIT } from "#ui/run-history-ui-handler";
import { executeIf, fixedInt, getEnumKeys, isLocal, NumberHolder, randInt, randSeedItem } from "#utils/common";
import { executeIf, fixedInt, isLocal, NumberHolder, randInt, randSeedItem } from "#utils/common";
import { decrypt, encrypt } from "#utils/data";
import { getEnumKeys } from "#utils/enums";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import { AES, enc } from "crypto-js";
import i18next from "i18next";

View File

@ -2,7 +2,8 @@ import { pokerogueApi } from "#api/pokerogue-api";
import { globalScene } from "#app/global-scene";
import { addTextObject, TextStyle } from "#ui/text";
import { addWindow, WindowVariant } from "#ui/ui-theme";
import { executeIf, getEnumKeys } from "#utils/common";
import { executeIf } from "#utils/common";
import { getEnumKeys } from "#utils/enums";
import i18next from "i18next";
export interface RankingEntry {

View File

@ -11,7 +11,8 @@ import { getVoucherTypeIcon, VoucherType } from "#system/voucher";
import { MessageUiHandler } from "#ui/message-ui-handler";
import { addTextObject, getEggTierTextTint, getTextStyleOptions, TextStyle } from "#ui/text";
import { addWindow } from "#ui/ui-theme";
import { fixedInt, getEnumValues, randSeedShuffle } from "#utils/common";
import { fixedInt, randSeedShuffle } from "#utils/common";
import { getEnumValues } from "#utils/enums";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import i18next from "i18next";

View File

@ -13,8 +13,9 @@ import { BgmBar } from "#ui/bgm-bar";
import { MessageUiHandler } from "#ui/message-ui-handler";
import { addTextObject, getTextStyleOptions, TextStyle } from "#ui/text";
import { addWindow, WindowVariant } from "#ui/ui-theme";
import { fixedInt, getEnumKeys, isLocal, sessionIdKey } from "#utils/common";
import { fixedInt, isLocal, sessionIdKey } from "#utils/common";
import { getCookie } from "#utils/cookies";
import { getEnumValues } from "#utils/enums";
import { isBeta } from "#utils/utility-vars";
import i18next from "i18next";
@ -76,11 +77,9 @@ export class MenuUiHandler extends MessageUiHandler {
{ condition: bypassLogin, options: [MenuOptions.LOG_OUT] },
];
this.menuOptions = getEnumKeys(MenuOptions)
.map(m => Number.parseInt(MenuOptions[m]) as MenuOptions)
.filter(m => {
return !this.excludedMenus().some(exclusion => exclusion.condition && exclusion.options.includes(m));
});
this.menuOptions = getEnumValues(MenuOptions).filter(m => {
return !this.excludedMenus().some(exclusion => exclusion.condition && exclusion.options.includes(m));
});
}
setup(): void {
@ -131,11 +130,9 @@ export class MenuUiHandler extends MessageUiHandler {
{ condition: bypassLogin, options: [MenuOptions.LOG_OUT] },
];
this.menuOptions = getEnumKeys(MenuOptions)
.map(m => Number.parseInt(MenuOptions[m]) as MenuOptions)
.filter(m => {
return !this.excludedMenus().some(exclusion => exclusion.condition && exclusion.options.includes(m));
});
this.menuOptions = getEnumValues(MenuOptions).filter(m => {
return !this.excludedMenus().some(exclusion => exclusion.condition && exclusion.options.includes(m));
});
this.optionSelectText = addTextObject(
0,
@ -511,11 +508,9 @@ export class MenuUiHandler extends MessageUiHandler {
this.render();
super.show(args);
this.menuOptions = getEnumKeys(MenuOptions)
.map(m => Number.parseInt(MenuOptions[m]) as MenuOptions)
.filter(m => {
return !this.excludedMenus().some(exclusion => exclusion.condition && exclusion.options.includes(m));
});
this.menuOptions = getEnumValues(MenuOptions).filter(m => {
return !this.excludedMenus().some(exclusion => exclusion.condition && exclusion.options.includes(m));
});
this.menuContainer.setVisible(true);
this.setCursor(0);

View File

@ -269,13 +269,22 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler {
globalScene.updateBiomeWaveText();
globalScene.updateMoneyText();
// DO NOT REMOVE: Fixes bug which allows action input to be processed before the UI is shown,
// causing errors if reroll is selected
this.awaitingActionInput = false;
// TODO: Replace with `Promise.withResolvers` when possible.
let tweenResolve: () => void;
const tweenPromise = new Promise<void>(resolve => (tweenResolve = resolve));
let i = 0;
// TODO: Rework this bespoke logic for animating the modifier options.
globalScene.tweens.addCounter({
ease: "Sine.easeIn",
duration: 1250,
onUpdate: t => {
const value = t.getValue();
// The bang here is safe, as `getValue()` only returns null if the tween has been destroyed (which obviously isn't the case inside onUpdate)
const value = t.getValue()!;
const index = Math.floor(value * typeOptions.length);
if (index > i && index <= typeOptions.length) {
const option = this.options[i];
@ -286,67 +295,77 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler {
i++;
}
},
onComplete: () => {
tweenResolve();
},
});
globalScene.time.delayedCall(1000 + maxUpgradeCount * 2000, () => {
for (const shopOption of this.shopOptionsRows.flat()) {
shopOption.show(0, 0);
}
let shopResolve: () => void;
const shopPromise = new Promise<void>(resolve => (shopResolve = resolve));
tweenPromise.then(() => {
globalScene.time.delayedCall(1000, () => {
for (const shopOption of this.shopOptionsRows.flat()) {
shopOption.show(0, 0);
}
shopResolve();
});
});
globalScene.time.delayedCall(4000 + maxUpgradeCount * 2000, () => {
if (partyHasHeldItem) {
this.transferButtonContainer.setAlpha(0);
this.transferButtonContainer.setVisible(true);
shopPromise.then(() => {
globalScene.time.delayedCall(500, () => {
if (partyHasHeldItem) {
this.transferButtonContainer.setAlpha(0);
this.transferButtonContainer.setVisible(true);
globalScene.tweens.add({
targets: this.transferButtonContainer,
alpha: 1,
duration: 250,
});
}
this.rerollButtonContainer.setAlpha(0);
this.checkButtonContainer.setAlpha(0);
this.lockRarityButtonContainer.setAlpha(0);
this.continueButtonContainer.setAlpha(0);
this.rerollButtonContainer.setVisible(true);
this.checkButtonContainer.setVisible(true);
this.continueButtonContainer.setVisible(this.rerollCost < 0);
this.lockRarityButtonContainer.setVisible(canLockRarities);
globalScene.tweens.add({
targets: this.transferButtonContainer,
targets: [this.checkButtonContainer, this.continueButtonContainer],
alpha: 1,
duration: 250,
});
}
this.rerollButtonContainer.setAlpha(0);
this.checkButtonContainer.setAlpha(0);
this.lockRarityButtonContainer.setAlpha(0);
this.continueButtonContainer.setAlpha(0);
this.rerollButtonContainer.setVisible(true);
this.checkButtonContainer.setVisible(true);
this.continueButtonContainer.setVisible(this.rerollCost < 0);
this.lockRarityButtonContainer.setVisible(canLockRarities);
globalScene.tweens.add({
targets: [this.rerollButtonContainer, this.lockRarityButtonContainer],
alpha: this.rerollCost < 0 ? 0.5 : 1,
duration: 250,
});
globalScene.tweens.add({
targets: [this.checkButtonContainer, this.continueButtonContainer],
alpha: 1,
duration: 250,
});
const updateCursorTarget = () => {
if (globalScene.shopCursorTarget === ShopCursorTarget.CHECK_TEAM) {
this.setRowCursor(0);
this.setCursor(2);
} else if (globalScene.shopCursorTarget === ShopCursorTarget.SHOP && globalScene.gameMode.hasNoShop) {
this.setRowCursor(ShopCursorTarget.REWARDS);
this.setCursor(0);
} else {
this.setRowCursor(globalScene.shopCursorTarget);
this.setCursor(0);
}
};
globalScene.tweens.add({
targets: [this.rerollButtonContainer, this.lockRarityButtonContainer],
alpha: this.rerollCost < 0 ? 0.5 : 1,
duration: 250,
});
updateCursorTarget();
const updateCursorTarget = () => {
if (globalScene.shopCursorTarget === ShopCursorTarget.CHECK_TEAM) {
this.setRowCursor(0);
this.setCursor(2);
} else if (globalScene.shopCursorTarget === ShopCursorTarget.SHOP && globalScene.gameMode.hasNoShop) {
this.setRowCursor(ShopCursorTarget.REWARDS);
this.setCursor(0);
} else {
this.setRowCursor(globalScene.shopCursorTarget);
this.setCursor(0);
}
};
updateCursorTarget();
handleTutorial(Tutorial.Select_Item).then(res => {
if (res) {
updateCursorTarget();
}
this.awaitingActionInput = true;
this.onActionInput = args[2];
handleTutorial(Tutorial.Select_Item).then(res => {
if (res) {
updateCursorTarget();
}
this.awaitingActionInput = true;
this.onActionInput = args[2];
});
});
});
@ -687,7 +706,11 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler {
scale: 0.01,
duration: 250,
ease: "Cubic.easeIn",
onComplete: () => options.forEach(o => o.destroy()),
onComplete: () => {
options.forEach(o => {
o.destroy();
});
},
});
[
@ -819,7 +842,7 @@ class ModifierOption extends Phaser.GameObjects.Container {
if (!globalScene) {
return;
}
const value = t.getValue();
const value = t.getValue()!;
if (!bounce && value > lastValue) {
globalScene.playSound("se/pb_bounce_1", {
volume: 1 / ++bounceCount,
@ -832,41 +855,38 @@ class ModifierOption extends Phaser.GameObjects.Container {
},
});
// TODO: Figure out proper delay between chains and then convert this into a single tween chain
// rather than starting multiple tween chains.
for (let u = 0; u < this.modifierTypeOption.upgradeCount; u++) {
const upgradeIndex = u;
globalScene.time.delayedCall(
remainingDuration - 2000 * (this.modifierTypeOption.upgradeCount - (upgradeIndex + 1 + upgradeCountOffset)),
() => {
globalScene.playSound("se/upgrade", {
rate: 1 + 0.25 * upgradeIndex,
});
this.pbTint.setPosition(this.pb.x, this.pb.y);
this.pbTint.setTintFill(0xffffff);
this.pbTint.setAlpha(0);
this.pbTint.setVisible(true);
globalScene.tweens.add({
globalScene.tweens.chain({
tweens: [
{
delay: remainingDuration - 2000 * (this.modifierTypeOption.upgradeCount - (u + 1 + upgradeCountOffset)),
onStart: () => {
globalScene.playSound("se/upgrade", {
rate: 1 + 0.25 * u,
});
this.pbTint.setPosition(this.pb.x, this.pb.y).setTintFill(0xffffff).setVisible(true).setAlpha(0);
},
targets: this.pbTint,
alpha: 1,
duration: 1000,
ease: "Sine.easeIn",
onComplete: () => {
this.pb.setTexture(
"pb",
this.getPbAtlasKey(-this.modifierTypeOption.upgradeCount + (upgradeIndex + 1)),
);
globalScene.tweens.add({
targets: this.pbTint,
alpha: 0,
duration: 750,
ease: "Sine.easeOut",
onComplete: () => {
this.pbTint.setVisible(false);
},
});
this.pb.setTexture("pb", this.getPbAtlasKey(-this.modifierTypeOption.upgradeCount + (u + 1)));
},
});
},
);
},
{
targets: this.pbTint,
alpha: 0,
duration: 750,
ease: "Sine.easeOut",
onComplete: () => {
this.pbTint.setVisible(false);
},
},
],
});
}
}

View File

@ -55,13 +55,13 @@ import { addBBCodeTextObject, addTextObject, getTextColor, getTextStyleOptions,
import { addWindow } from "#ui/ui-theme";
import {
BooleanHolder,
getEnumKeys,
getLocalizedSpriteKey,
isNullOrUndefined,
padInt,
rgbHexToRgba,
toReadableString,
} from "#utils/common";
import { getEnumValues } from "#utils/enums";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import { argbFromRgba } from "@material/material-color-utilities";
import i18next from "i18next";
@ -640,7 +640,7 @@ export class PokedexPageUiHandler extends MessageUiHandler {
this.menuContainer.setVisible(false);
this.menuOptions = getEnumKeys(MenuOptions).map(m => Number.parseInt(MenuOptions[m]) as MenuOptions);
this.menuOptions = getEnumValues(MenuOptions);
this.optionSelectText = addBBCodeTextObject(
0,
@ -744,7 +744,7 @@ export class PokedexPageUiHandler extends MessageUiHandler {
this.starterAttributes = this.initStarterPrefs();
this.menuOptions = getEnumKeys(MenuOptions).map(m => Number.parseInt(MenuOptions[m]) as MenuOptions);
this.menuOptions = getEnumValues(MenuOptions);
this.menuContainer.setVisible(true);

View File

@ -147,7 +147,7 @@ export class StatsContainer extends Phaser.GameObjects.Container {
duration: 1000,
ease: "Cubic.easeOut",
onUpdate: (tween: Phaser.Tweens.Tween) => {
const progress = tween.getValue();
const progress = tween.getValue() ?? 1;
const interpolatedData = ivChartData.map(
(v: number, i: number) => v * progress + lastIvChartData[i] * (1 - progress),
);

View File

@ -29,7 +29,6 @@ import { UiHandler } from "#ui/ui-handler";
import {
fixedInt,
formatStat,
getEnumValues,
getLocalizedSpriteKey,
getShinyDescriptor,
isNullOrUndefined,
@ -37,6 +36,7 @@ import {
rgbHexToRgba,
toReadableString,
} from "#utils/common";
import { getEnumValues } from "#utils/enums";
import { argbFromRgba } from "@material/material-color-utilities";
import i18next from "i18next";

View File

@ -157,8 +157,8 @@ export class TargetSelectUiHandler extends UiHandler {
yoyo: true,
onUpdate: t => {
for (const target of this.targetsHighlighted) {
target.setAlpha(t.getValue());
this.highlightItems(target.id, t.getValue());
target.setAlpha(t.getValue() ?? 1);
this.highlightItems(target.id, t.getValue() ?? 1);
}
},
});

View File

@ -5,7 +5,7 @@ import { UiTheme } from "#enums/ui-theme";
import i18next from "#plugins/i18n";
import type Phaser from "phaser";
import BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText";
import InputText from "phaser3-rex-plugins/plugins/inputtext";
import type InputText from "phaser3-rex-plugins/plugins/inputtext";
export enum TextStyle {
MESSAGE,
@ -152,8 +152,7 @@ export function addTextInputObject(
): InputText {
const { scale, styleOptions } = getTextStyleOptions(style, globalScene.uiTheme, extraStyleOptions);
const ret = new InputText(globalScene, x, y, width, height, styleOptions as InputText.IConfig);
globalScene.add.existing(ret);
const ret = globalScene.add.rexInputText(x, y, width, height, styleOptions as InputText.IConfig);
ret.setScale(scale);
return ret;

View File

@ -8,11 +8,19 @@ export type nil = null | undefined;
export const MissingTextureKey = "__MISSING";
// TODO: Draft tests for these utility functions
// TODO: Break up this file
/**
* Convert a `snake_case` string in any capitalization (such as one from an enum reverse mapping)
* into a readable `Title Case` version.
* @param str - The snake case string to be converted.
* @returns The result of converting `str` into title case.
*/
export function toReadableString(str: string): string {
return str
.replace(/_/g, " ")
.split(" ")
.map(s => `${s.slice(0, 1)}${s.slice(1).toLowerCase()}`)
.map(s => capitalizeFirstLetter(s.toLowerCase()))
.join(" ");
}
@ -273,18 +281,6 @@ export function formatStat(stat: number, forHp = false): string {
return formatLargeNumber(stat, forHp ? 100000 : 1000000);
}
export function getEnumKeys(enumType: any): string[] {
return Object.values(enumType)
.filter(v => Number.isNaN(Number.parseInt(v!.toString())))
.map(v => v!.toString());
}
export function getEnumValues(enumType: any): number[] {
return Object.values(enumType)
.filter(v => !Number.isNaN(Number.parseInt(v!.toString())))
.map(v => Number.parseInt(v!.toString()));
}
export function executeIf<T>(condition: boolean, promiseFunc: () => Promise<T>): Promise<T | null> {
return condition ? promiseFunc() : new Promise<T | null>(resolve => resolve(null));
}
@ -644,25 +640,3 @@ export function coerceArray<T>(input: T): T extends any[] ? T : [T];
export function coerceArray<T>(input: T): T | [T] {
return Array.isArray(input) ? input : [input];
}
/**
* Returns the name of the key that matches the enum [object] value.
* @param input - The enum [object] to check
* @param val - The value to get the key of
* @returns The name of the key with the specified value
* @example
* const thing = {
* one: 1,
* two: 2,
* } as const;
* console.log(enumValueToKey(thing, thing.two)); // output: "two"
* @throws An `Error` if an invalid enum value is passed to the function
*/
export function enumValueToKey<T extends Record<string, string | number>>(input: T, val: T[keyof T]): keyof T {
for (const [key, value] of Object.entries(input)) {
if (val === value) {
return key as keyof T;
}
}
throw new Error(`Invalid value passed to \`enumValueToKey\`! Value: ${val}`);
}

74
src/utils/enums.ts Normal file
View File

@ -0,0 +1,74 @@
import type { EnumOrObject, EnumValues, NormalEnum, TSNumericEnum } from "#app/@types/enum-types";
import type { InferKeys } from "#app/@types/type-helpers";
/**
* Return the string keys of an Enum object, excluding reverse-mapped numbers.
* @param enumType - The numeric enum to retrieve keys for
* @returns An ordered array of all of `enumType`'s string keys
* @example
* enum fruit {
* apple = 1,
* banana = 2,
* cherry = 3,
* orange = 12,
* };
*
* console.log(getEnumKeys<typeof fruit>(fruit)); // output: ["apple", "banana", "cherry", "orange"]
* @remarks
* To retrieve the keys of a {@linkcode NormalEnum}, use {@linkcode Object.keys} instead.
*/
export function getEnumKeys<E extends EnumOrObject>(enumType: TSNumericEnum<E>): (keyof E)[] {
// All enum values are either normal numbers or reverse mapped strings, so we can retrieve the keys by filtering out numbers.
return Object.values(enumType).filter(v => typeof v === "string");
}
/**
* Return the numeric values of a numeric Enum object, excluding reverse-mapped strings.
* @param enumType - The enum object to retrieve keys for
* @returns An ordered array of all of `enumType`'s number values
* @example
* enum fruit {
* apple = 1,
* banana = 2,
* cherry = 3,
* orange = 12,
* };
*
* console.log(getEnumValues<typeof fruit>(fruit)); // output: [1, 2, 3, 12]
*
* @remarks
* To retrieve the keys of a {@linkcode NormalEnum}, use {@linkcode Object.values} instead.
*/
// NB: This intentionally does not use `EnumValues<E>` as using `E[keyof E]` leads to improved variable highlighting in IDEs.
export function getEnumValues<E extends EnumOrObject>(enumType: TSNumericEnum<E>): E[keyof E][] {
return Object.values(enumType).filter(v => typeof v !== "string") as E[keyof E][];
}
/**
* Return the name of the key that matches the given Enum value.
* Can be used to emulate Typescript reverse mapping for `const object`s or string enums.
* @param object - The {@linkcode NormalEnum} to check
* @param val - The value to get the key of
* @returns The name of the key with the specified value
* @example
* const thing = {
* one: 1,
* two: 2,
* } as const;
* console.log(enumValueToKey(thing, 2)); // output: "two"
* @throws Error if an invalid enum value is passed to the function
* @remarks
* If multiple keys map to the same value, the first one (in insertion order) will be retrieved,
* but the return type will be the union of ALL their corresponding keys.
*/
export function enumValueToKey<T extends EnumOrObject, V extends EnumValues<T>>(
object: NormalEnum<T>,
val: V,
): InferKeys<T, V> {
for (const [key, value] of Object.entries(object)) {
if (val === value) {
return key as InferKeys<T, V>;
}
}
throw new Error(`Invalid value passed to \`enumValueToKey\`! Value: ${val}`);
}

View File

@ -159,7 +159,8 @@ describe("Abilities - Magic Bounce", () => {
expect(game.scene.getEnemyPokemon()!.getTag(BattlerTagType.CURSED)).toBeDefined();
});
it("should not cause encore to be interrupted after bouncing", async () => {
// TODO: enable when Magic Bounce is fixed to properly reset the hit count
it.todo("should not cause encore to be interrupted after bouncing", async () => {
game.override.moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE]).enemyMoveset([MoveId.TACKLE, MoveId.GROWL]);
// game.override.ability(AbilityId.MOLD_BREAKER);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -167,7 +168,7 @@ describe("Abilities - Magic Bounce", () => {
const enemyPokemon = game.scene.getEnemyPokemon()!;
// Give the player MOLD_BREAKER for this turn to bypass Magic Bounce.
vi.spyOn(playerPokemon, "getAbility").mockReturnValue(allAbilities[AbilityId.MOLD_BREAKER]);
const playerAbilitySpy = game.field.mockAbility(playerPokemon, AbilityId.MOLD_BREAKER);
// turn 1
game.move.select(MoveId.ENCORE);
@ -177,7 +178,7 @@ describe("Abilities - Magic Bounce", () => {
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(MoveId.TACKLE);
// turn 2
vi.spyOn(playerPokemon, "getAbility").mockRestore();
playerAbilitySpy.mockRestore();
game.move.select(MoveId.GROWL);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("BerryPhase");

View File

@ -101,7 +101,8 @@ describe("BattlerTag - StockpilingTag", () => {
});
describe("stack limit, stat tracking, and removal", () => {
it("can be added up to three times, even when one stat does not change", async () => {
// TODO: do we even want this file at all? regardless, this test is broken and is also likely unimportant
it.todo("can be added up to three times, even when one stat does not change", async () => {
const mockPokemon = {
summonData: new PokemonSummonData(),
getBattlerIndex: () => 0,
@ -150,7 +151,7 @@ describe("BattlerTag - StockpilingTag", () => {
expect(subject.stockpiledCount).toBe(3);
vi.spyOn(game.scene.phaseManager, "unshiftPhase").mockImplementationOnce(_phase => {
throw new Error("Should not be called a fourth time");
expect.fail("Should not be called a fourth time");
});
// fourth stack should not be applied

View File

@ -1,6 +1,5 @@
import { allMoves } from "#data/data-lists";
import { AbilityId } from "#enums/ability-id";
import { BattleType } from "#enums/battle-type";
import { BattlerIndex } from "#enums/battler-index";
import { MoveId } from "#enums/move-id";
import { MoveUseMode } from "#enums/move-use-mode";
@ -28,7 +27,7 @@ describe("Moves - Fishious Rend & Bolt Beak", () => {
game.override
.ability(AbilityId.STURDY)
.battleStyle("single")
.battleType(BattleType.TRAINER)
.startingWave(5)
.criticalHits(false)
.enemyLevel(100)
.enemySpecies(SpeciesId.DRACOVISH)

View File

@ -20,8 +20,8 @@ import {
} from "#test/mystery-encounter/encounter-test-utils";
import { GameManager } from "#test/testUtils/gameManager";
import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils";
import { TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#trainers/TrainerPartyTemplate";
import { TrainerConfig } from "#trainers/trainer-config";
import { TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#trainers/trainer-party-template";
import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";

View File

@ -1,4 +1,6 @@
import type { MockGameObject } from "#test/testUtils/mocks/mockGameObject";
import { MockBBCodeText } from "#test/testUtils/mocks/mocksContainer/mock-bbcode-text";
import { MockInputText } from "#test/testUtils/mocks/mocksContainer/mock-input-text";
import { MockContainer } from "#test/testUtils/mocks/mocksContainer/mockContainer";
import { MockImage } from "#test/testUtils/mocks/mocksContainer/mockImage";
import { MockNineslice } from "#test/testUtils/mocks/mocksContainer/mockNineslice";
@ -33,6 +35,8 @@ export class MockTextureManager {
image: this.image.bind(this),
polygon: this.polygon.bind(this),
text: this.text.bind(this),
rexBBCodeText: this.rexBBCodeText.bind(this),
rexInputText: this.rexInputText.bind(this),
bitmapText: this.text.bind(this),
displayList: this.displayList,
video: () => new MockVideoGameObject(),
@ -103,9 +107,25 @@ export class MockTextureManager {
return text;
}
rexBBCodeText(x, y, content, styleOptions) {
const text = new MockBBCodeText(this, x, y, content, styleOptions);
this.list.push(text);
return text;
}
rexInputText(x, y, w, h, content, styleOptions) {
const text = new MockInputText(this, x, y, w, h, 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;
}
exists(key: string): boolean {
return this.textures.has(key);
}
}

View File

@ -0,0 +1,6 @@
import { MockText } from "#test/testUtils/mocks/mocksContainer/mockText";
export class MockBBCodeText extends MockText {
setMaxLines(_lines: number) {}
setWrapMode(_mode: 0 | 1 | 2 | 3 | "none" | "word" | "char" | "character" | "mix") {}
}

View File

@ -0,0 +1,24 @@
import { MockText } from "#test/testUtils/mocks/mocksContainer/mockText";
export class MockInputText extends MockText {
public inputType: string;
public selectionStart: number;
public selectionEnd: number;
public selectedText: string;
constructor(textureManager, x, y, _w, _h, content, styleOptions) {
super(textureManager, x, y, content, styleOptions);
}
selectText(_selectionStart?: number, _selectionEnd?: number) {}
selectAll() {}
setCursorPosition(_value: number) {}
scrollToBottom() {}
resize(_width: number, _height: number) {}
setElement(_element, _style, _innerText) {}
}

View File

@ -0,0 +1,104 @@
import type { EnumOrObject, EnumValues, NormalEnum, TSNumericEnum } from "#app/@types/enum-types";
import type { enumValueToKey, getEnumKeys, getEnumValues } from "#app/utils/enums";
import { describe, expectTypeOf, it } from "vitest";
enum testEnumNum {
testN1 = 1,
testN2 = 2,
}
enum testEnumString {
testS1 = "apple",
testS2 = "banana",
}
const testObjNum = { testON1: 1, testON2: 2 } as const;
const testObjString = { testOS1: "apple", testOS2: "banana" } as const;
describe("Enum Type Helpers", () => {
describe("EnumValues", () => {
it("should go from enum object type to value type", () => {
expectTypeOf<EnumValues<typeof testEnumNum>>().toEqualTypeOf<testEnumNum>();
expectTypeOf<EnumValues<typeof testEnumNum>>().branded.toEqualTypeOf<1 | 2>();
expectTypeOf<EnumValues<typeof testEnumString>>().toEqualTypeOf<testEnumString>();
expectTypeOf<EnumValues<typeof testEnumString>>().toEqualTypeOf<testEnumString.testS1 | testEnumString.testS2>();
expectTypeOf<EnumValues<typeof testEnumString>>().toMatchTypeOf<"apple" | "banana">();
});
it("should produce union of const object values as type", () => {
expectTypeOf<EnumValues<typeof testObjNum>>().toEqualTypeOf<1 | 2>();
expectTypeOf<EnumValues<typeof testObjString>>().toEqualTypeOf<"apple" | "banana">();
});
});
describe("TSNumericEnum", () => {
it("should match numeric enums", () => {
expectTypeOf<TSNumericEnum<typeof testEnumNum>>().toEqualTypeOf<typeof testEnumNum>();
});
it("should not match string enums or const objects", () => {
expectTypeOf<TSNumericEnum<typeof testEnumString>>().toBeNever();
expectTypeOf<TSNumericEnum<typeof testObjNum>>().toBeNever();
expectTypeOf<TSNumericEnum<typeof testObjString>>().toBeNever();
});
});
describe("NormalEnum", () => {
it("should match string enums or const objects", () => {
expectTypeOf<NormalEnum<typeof testEnumString>>().toEqualTypeOf<typeof testEnumString>();
expectTypeOf<NormalEnum<typeof testObjNum>>().toEqualTypeOf<typeof testObjNum>();
expectTypeOf<NormalEnum<typeof testObjString>>().toEqualTypeOf<typeof testObjString>();
});
it("should not match numeric enums", () => {
expectTypeOf<NormalEnum<typeof testEnumNum>>().toBeNever();
});
});
describe("EnumOrObject", () => {
it("should match any enum or const object", () => {
expectTypeOf<typeof testEnumNum>().toMatchTypeOf<EnumOrObject>();
expectTypeOf<typeof testEnumString>().toMatchTypeOf<EnumOrObject>();
expectTypeOf<typeof testObjNum>().toMatchTypeOf<EnumOrObject>();
expectTypeOf<typeof testObjString>().toMatchTypeOf<EnumOrObject>();
});
it("should not match an enum value union w/o typeof", () => {
expectTypeOf<testEnumNum>().not.toMatchTypeOf<EnumOrObject>();
expectTypeOf<testEnumString>().not.toMatchTypeOf<EnumOrObject>();
});
it("should be equivalent to `TSNumericEnum | NormalEnum`", () => {
expectTypeOf<EnumOrObject>().branded.toEqualTypeOf<TSNumericEnum<EnumOrObject> | NormalEnum<EnumOrObject>>();
});
});
});
describe("Enum Functions", () => {
describe("getEnumKeys", () => {
it("should retrieve keys of numeric enum", () => {
expectTypeOf<typeof getEnumKeys<typeof testEnumNum>>().returns.toEqualTypeOf<("testN1" | "testN2")[]>();
});
});
describe("getEnumValues", () => {
it("should retrieve values of numeric enum", () => {
expectTypeOf<typeof getEnumValues<typeof testEnumNum>>().returns.branded.toEqualTypeOf<(1 | 2)[]>();
});
});
describe("enumValueToKey", () => {
it("should retrieve values for a given key", () => {
expectTypeOf<
typeof enumValueToKey<typeof testEnumString, testEnumString.testS1>
>().returns.toEqualTypeOf<"testS1">();
expectTypeOf<typeof enumValueToKey<typeof testEnumString, testEnumString>>().returns.toEqualTypeOf<
"testS1" | "testS2"
>();
expectTypeOf<typeof enumValueToKey<typeof testObjNum, 1>>().returns.toEqualTypeOf<"testON1">();
expectTypeOf<typeof enumValueToKey<typeof testObjNum, 1 | 2>>().returns.toEqualTypeOf<"testON1" | "testON2">();
});
});
});

View File

@ -47,11 +47,5 @@
"outDir": "./build",
"noEmit": true
},
"typedocOptions": {
"entryPoints": ["./src"],
"entryPointStrategy": "expand",
"exclude": "**/*+.test.ts",
"out": "typedoc"
},
"exclude": ["node_modules", "dist", "vite.config.ts", "vitest.config.ts", "vitest.workspace.ts"]
}

14
tsdoc.json Normal file
View File

@ -0,0 +1,14 @@
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/tsdoc/v0/tsdoc.schema.json",
"noStandardTags": false,
"tagDefinitions": [
{
"tagName": "@todo",
"syntaxKind": "block"
},
{
"tagName": "@linkcode",
"syntaxKind": "inline"
}
]
}

7
typedoc.json Normal file
View File

@ -0,0 +1,7 @@
{
"entryPoints": ["./src"],
"entryPointStrategy": "expand",
"exclude": ["**/*+.test.ts"],
"out": "typedoc",
"highlightLanguages": ["javascript", "json", "jsonc", "json5", "tsx", "typescript", "markdown"]
}

View File

@ -2,31 +2,13 @@ import { defineProject } from "vitest/config";
import { BaseSequencer, type TestSpecification } from "vitest/node";
import { defaultConfig } from "./vite.config";
function getTestOrder(testName: string): number {
if (testName.includes("battle-scene.test.ts")) {
return 1;
}
if (testName.includes("inputs.test.ts")) {
return 2;
}
return 3;
}
export default defineProject(({ mode }) => ({
...defaultConfig,
test: {
testTimeout: 20000,
setupFiles: ["./test/fontFace.setup.ts", "./test/vitest.setup.ts"],
sequence: {
sequencer: class CustomSequencer extends BaseSequencer {
async sort(files: TestSpecification[]) {
// use default sorting at first.
files = await super.sort(files);
// Except, forcibly reorder
return files.sort((a, b) => getTestOrder(a.moduleId) - getTestOrder(b.moduleId));
}
},
sequencer: MySequencer,
},
environment: "jsdom" as const,
environmentOptions: {
@ -34,6 +16,10 @@ export default defineProject(({ mode }) => ({
resources: "usable",
},
},
typecheck: {
tsconfig: "tsconfig.json",
include: ["./test/types/**/*.{test,spec}{-|.}d.ts"],
},
threads: false,
trace: true,
restoreMocks: true,
@ -51,3 +37,38 @@ export default defineProject(({ mode }) => ({
keepNames: true,
},
}));
//#region Helpers
/**
* Class for sorting test files in the desired order.
*/
class MySequencer extends BaseSequencer {
async sort(files: TestSpecification[]) {
files = await super.sort(files);
return files.sort((a, b) => {
const aTestOrder = getTestOrder(a.moduleId);
const bTestOrder = getTestOrder(b.moduleId);
return aTestOrder - bTestOrder;
});
}
}
/**
* A helper function for sorting test files in a desired order.
*
* A lower number means that a test file must be run earlier,
* or else it breaks due to running tests with `--no-isolate.`
*/
function getTestOrder(testName: string): number {
if (testName.includes("battle-scene.test.ts")) {
return 1;
}
if (testName.includes("inputs.test.ts")) {
return 2;
}
return 3;
}
//#endregion

View File

@ -1,14 +0,0 @@
import { defineWorkspace } from "vitest/config";
import { defaultConfig } from "./vite.config";
export default defineWorkspace([
{
...defaultConfig,
test: {
name: "pre",
include: ["./test/pre.test.ts"],
environment: "jsdom",
},
},
"./vitest.config.ts",
]);