mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-09-23 15:03:24 +02:00
Merge remote-tracking branch 'upstream/beta' into todo-test-enable
This commit is contained in:
commit
60c7581e14
27
biome.jsonc
27
biome.jsonc
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.2.3/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
@ -98,7 +98,9 @@
|
|||||||
"useTrimStartEnd": "error",
|
"useTrimStartEnd": "error",
|
||||||
"useReadonlyClassProperties": {
|
"useReadonlyClassProperties": {
|
||||||
"level": "info", // TODO: Graduate to error eventually
|
"level": "info", // TODO: Graduate to error eventually
|
||||||
"options": { "checkAllProperties": true }
|
// NOTE: "checkAllProperties" has an immature implementation that
|
||||||
|
// causes many false positives across files. Enable if/when maturity improves
|
||||||
|
"options": { "checkAllProperties": false }
|
||||||
},
|
},
|
||||||
"useConsistentObjectDefinitions": {
|
"useConsistentObjectDefinitions": {
|
||||||
"level": "error",
|
"level": "error",
|
||||||
@ -209,11 +211,15 @@
|
|||||||
"nursery": {
|
"nursery": {
|
||||||
"noUselessUndefined": "error",
|
"noUselessUndefined": "error",
|
||||||
"useMaxParams": {
|
"useMaxParams": {
|
||||||
"level": "warn", // TODO: Change to "error"... eventually...
|
"level": "info", // TODO: Change to "error"... eventually...
|
||||||
"options": { "max": 4 } // A lot of stuff has a few params, but
|
"options": { "max": 7 }
|
||||||
},
|
},
|
||||||
"noShadow": "warn", // TODO: refactor and make "error"
|
"noShadow": "warn", // TODO: refactor and make "error"
|
||||||
"noNonNullAssertedOptionalChain": "warn" // TODO: refactor and make "error"
|
"noNonNullAssertedOptionalChain": "warn", // TODO: refactor and make "error"
|
||||||
|
"noDuplicateDependencies": "error",
|
||||||
|
"noImportCycles": "error",
|
||||||
|
// TODO: Change to error once promises are used properly
|
||||||
|
"noMisusedPromises": "info"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -248,16 +254,9 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Overrides to prevent unused import removal inside `overrides.ts`, enums & `.d.ts` files (for TSDoc linkcodes),
|
// Overrides to prevent unused import removal inside `overrides.ts`, enums & `.d.ts` files (for TSDoc linkcodes),
|
||||||
// as well as inside script boilerplate files.
|
// as well as inside script boilerplate files (whose imports will _presumably_ be used in the generated file).
|
||||||
{
|
{
|
||||||
// TODO: Rename existing boilerplates in the folder and remove this last alias
|
"includes": ["**/src/overrides.ts", "**/src/enums/**/*", "**/*.d.ts", "scripts/**/*.boilerplate.ts"],
|
||||||
"includes": [
|
|
||||||
"**/src/overrides.ts",
|
|
||||||
"**/src/enums/**/*",
|
|
||||||
"**/*.d.ts",
|
|
||||||
"scripts/**/*.boilerplate.ts",
|
|
||||||
"**/boilerplates/*.ts"
|
|
||||||
],
|
|
||||||
"linter": {
|
"linter": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"correctness": {
|
"correctness": {
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"start:podman": "vite --mode development --host 0.0.0.0 --port $PORT",
|
"start:podman": "vite --mode development --host 0.0.0.0 --port $PORT",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:beta": "vite build --mode beta",
|
"build:beta": "vite build --mode beta",
|
||||||
|
"build:dev": "vite build --mode development",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest run --no-isolate",
|
"test": "vitest run --no-isolate",
|
||||||
"test:cov": "vitest run --coverage --no-isolate",
|
"test:cov": "vitest run --coverage --no-isolate",
|
||||||
@ -32,7 +33,7 @@
|
|||||||
"update-locales:remote": "git submodule update --progress --init --recursive --force --remote"
|
"update-locales:remote": "git submodule update --progress --init --recursive --force --remote"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.2.3",
|
"@biomejs/biome": "2.2.4",
|
||||||
"@ls-lint/ls-lint": "2.3.1",
|
"@ls-lint/ls-lint": "2.3.1",
|
||||||
"@types/crypto-js": "^4.2.0",
|
"@types/crypto-js": "^4.2.0",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
@ -51,8 +52,8 @@
|
|||||||
"typedoc-github-theme": "^0.3.1",
|
"typedoc-github-theme": "^0.3.1",
|
||||||
"typedoc-plugin-coverage": "^4.0.1",
|
"typedoc-plugin-coverage": "^4.0.1",
|
||||||
"typedoc-plugin-mdn-links": "^5.0.9",
|
"typedoc-plugin-mdn-links": "^5.0.9",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.9.2",
|
||||||
"vite": "^7.0.6",
|
"vite": "^7.0.7",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
"vitest-canvas-mock": "^0.3.3"
|
"vitest-canvas-mock": "^0.3.3"
|
||||||
@ -73,5 +74,5 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.0.0"
|
"node": ">=22.0.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.14.0"
|
"packageManager": "pnpm@10.16.1"
|
||||||
}
|
}
|
||||||
|
591
pnpm-lock.yaml
591
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -47,6 +47,6 @@ describe("{{description}}", () => {
|
|||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expect(feebas).toHaveUsedMove({ move: MoveId.SPLASH, result: MoveResult.SUCCESS });
|
expect(feebas).toHaveUsedMove({ move: MoveId.SPLASH, result: MoveResult.SUCCESS });
|
||||||
expect(game.textInterceptor.logs).toContain(i18next.t("moveTriggers:splash"));
|
expect(game).toHaveShownMessage(i18next.t("moveTriggers:splash"));
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -102,9 +102,9 @@ async function promptFileName(selectedType) {
|
|||||||
function getBoilerplatePath(choiceType) {
|
function getBoilerplatePath(choiceType) {
|
||||||
switch (choiceType) {
|
switch (choiceType) {
|
||||||
// case "Reward":
|
// case "Reward":
|
||||||
// return path.join(__dirname, "boilerplates/reward.ts");
|
// return path.join(__dirname, "boilerplates/reward.boilerplate.ts");
|
||||||
default:
|
default:
|
||||||
return path.join(__dirname, "boilerplates/default.ts");
|
return path.join(__dirname, "boilerplates/default.boilerplate.ts");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
770
src/ai/ai-moveset-gen.ts
Normal file
770
src/ai/ai-moveset-gen.ts
Normal file
@ -0,0 +1,770 @@
|
|||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { speciesEggMoves } from "#balance/egg-moves";
|
||||||
|
import {
|
||||||
|
BASE_LEVEL_WEIGHT_OFFSET,
|
||||||
|
BASE_WEIGHT_MULTIPLIER,
|
||||||
|
BOSS_EXTRA_WEIGHT_MULTIPLIER,
|
||||||
|
COMMON_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
COMMON_TM_MOVESET_WEIGHT,
|
||||||
|
EGG_MOVE_LEVEL_REQUIREMENT,
|
||||||
|
EGG_MOVE_TO_LEVEL_WEIGHT,
|
||||||
|
EGG_MOVE_WEIGHT_MAX,
|
||||||
|
EVOLUTION_MOVE_WEIGHT,
|
||||||
|
GREAT_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
GREAT_TM_MOVESET_WEIGHT,
|
||||||
|
getMaxEggMoveCount,
|
||||||
|
getMaxTmCount,
|
||||||
|
RARE_EGG_MOVE_LEVEL_REQUIREMENT,
|
||||||
|
STAB_BLACKLIST,
|
||||||
|
ULTRA_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
ULTRA_TM_MOVESET_WEIGHT,
|
||||||
|
} from "#balance/moveset-generation";
|
||||||
|
import { EVOLVE_MOVE, RELEARN_MOVE } from "#balance/pokemon-level-moves";
|
||||||
|
import { speciesTmMoves, tmPoolTiers } from "#balance/tms";
|
||||||
|
import { allMoves } from "#data/data-lists";
|
||||||
|
import { ModifierTier } from "#enums/modifier-tier";
|
||||||
|
import { MoveCategory } from "#enums/move-category";
|
||||||
|
import type { MoveId } from "#enums/move-id";
|
||||||
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import type { SpeciesId } from "#enums/species-id";
|
||||||
|
import { Stat } from "#enums/stat";
|
||||||
|
import type { EnemyPokemon, Pokemon } from "#field/pokemon";
|
||||||
|
import { PokemonMove } from "#moves/pokemon-move";
|
||||||
|
import { NumberHolder, randSeedInt } from "#utils/common";
|
||||||
|
import { isBeta } from "#utils/utility-vars";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute and assign a weight to the level-up moves currently available to the Pokémon
|
||||||
|
*
|
||||||
|
* @param pokemon - The Pokémon to generate a level-based move pool for
|
||||||
|
* @returns A map of move IDs to their computed weights
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* A move's weight is determined by its level, as follows:
|
||||||
|
* 1. If the level is an {@linkcode EVOLVE_MOVE} move, weight is 60
|
||||||
|
* 2. If it is level 1 with 80+ BP, it is considered a "move reminder" move and
|
||||||
|
* weight is 60
|
||||||
|
* 3. If the Pokémon has a trainer and the move is a {@linkcode RELEARN_MOVE},
|
||||||
|
* weight is 60
|
||||||
|
* 4. Otherwise, weight is the earliest level the move can be learned + 20
|
||||||
|
*/
|
||||||
|
function getAndWeightLevelMoves(pokemon: Pokemon): Map<MoveId, number> {
|
||||||
|
const movePool = new Map<MoveId, number>();
|
||||||
|
let allLevelMoves: [number, MoveId][];
|
||||||
|
// TODO: Investigate why there needs to be error handling here
|
||||||
|
try {
|
||||||
|
allLevelMoves = pokemon.getLevelMoves(1, true, true, pokemon.hasTrainer());
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error encountered trying to generate moveset for %s: %s", pokemon.species.name, e);
|
||||||
|
return movePool;
|
||||||
|
}
|
||||||
|
|
||||||
|
const level = pokemon.level;
|
||||||
|
const hasTrainer = pokemon.hasTrainer();
|
||||||
|
|
||||||
|
for (const levelMove of allLevelMoves) {
|
||||||
|
const [learnLevel, id] = levelMove;
|
||||||
|
if (level < learnLevel) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const move = allMoves[id];
|
||||||
|
// Skip unimplemented moves or moves that are already in the pool
|
||||||
|
if (move.name.endsWith(" (N)") || movePool.has(id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let weight = learnLevel + BASE_LEVEL_WEIGHT_OFFSET;
|
||||||
|
switch (learnLevel) {
|
||||||
|
case EVOLVE_MOVE:
|
||||||
|
weight = EVOLUTION_MOVE_WEIGHT;
|
||||||
|
break;
|
||||||
|
// Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight. Trainers use actual relearn moves.
|
||||||
|
case 1:
|
||||||
|
if (move.power >= 80) {
|
||||||
|
weight = 60;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case RELEARN_MOVE:
|
||||||
|
if (hasTrainer) {
|
||||||
|
weight = 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
movePool.set(id, weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
return movePool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine which TM tiers a Pokémon can learn based on its level
|
||||||
|
* @param level - The level of the Pokémon
|
||||||
|
* @returns A tuple indicating whether the Pokémon can learn common, great, and ultra tier TMs
|
||||||
|
*/
|
||||||
|
function getAllowedTmTiers(level: number): [common: boolean, great: boolean, ultra: boolean] {
|
||||||
|
return [
|
||||||
|
level >= COMMON_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
level >= GREAT_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
level >= ULTRA_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the TMs that a species can learn based on its ID and formKey
|
||||||
|
* @param speciesId - The species ID of the Pokémon
|
||||||
|
* @param level - The level of the Pokémon
|
||||||
|
* @param formKey - The form key of the Pokémon
|
||||||
|
* @param levelPool - The current level-based move pool, to avoid duplicates
|
||||||
|
* @param tmPool - The TM move pool to add to, which will be modified in place
|
||||||
|
* @param allowedTiers - The tiers of TMs the Pokémon is allowed to learn
|
||||||
|
*
|
||||||
|
* @privateRemarks
|
||||||
|
* Split out from `getAndWeightTmMoves` to allow fusion species to add their TMs
|
||||||
|
* without duplicating code.
|
||||||
|
*/
|
||||||
|
function getTmPoolForSpecies(
|
||||||
|
speciesId: number,
|
||||||
|
level: number,
|
||||||
|
formKey: string,
|
||||||
|
levelPool: ReadonlyMap<MoveId, number>,
|
||||||
|
eggPool: ReadonlyMap<MoveId, number>,
|
||||||
|
tmPool: Map<MoveId, number>,
|
||||||
|
allowedTiers = getAllowedTmTiers(level),
|
||||||
|
): void {
|
||||||
|
const [allowCommon, allowGreat, allowUltra] = allowedTiers;
|
||||||
|
const tms = speciesTmMoves[speciesId];
|
||||||
|
// Species with no learnable TMs (e.g. Ditto) don't have entries in the `speciesTmMoves` object,
|
||||||
|
// so this is needed to avoid iterating over `undefined`
|
||||||
|
if (tms == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let moveId: MoveId;
|
||||||
|
for (const tm of tms) {
|
||||||
|
if (Array.isArray(tm)) {
|
||||||
|
if (tm[0] !== formKey) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
moveId = tm[1];
|
||||||
|
} else {
|
||||||
|
moveId = tm;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (levelPool.has(moveId) || eggPool.has(moveId) || tmPool.has(moveId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (tmPoolTiers[moveId]) {
|
||||||
|
case ModifierTier.COMMON:
|
||||||
|
allowCommon && tmPool.set(moveId, COMMON_TM_MOVESET_WEIGHT);
|
||||||
|
break;
|
||||||
|
case ModifierTier.GREAT:
|
||||||
|
allowGreat && tmPool.set(moveId, GREAT_TM_MOVESET_WEIGHT);
|
||||||
|
break;
|
||||||
|
case ModifierTier.ULTRA:
|
||||||
|
allowUltra && tmPool.set(moveId, ULTRA_TM_MOVESET_WEIGHT);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute and assign a weight to the TM moves currently available to the Pokémon
|
||||||
|
* @param pokemon - The Pokémon to generate a TM-based move pool for
|
||||||
|
* @param currentSet - The current movepool, to avoid duplicates
|
||||||
|
* @param tmPool - The TM move pool to add to, which will be modified in place
|
||||||
|
* @returns A map of move IDs to their computed weights
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* Only trainer pokemon can learn TM moves, and there are restrictions
|
||||||
|
* as to how many and which TMs are available based on the level of the Pokémon.
|
||||||
|
* 1. Before level 25, no TM moves are available
|
||||||
|
* 2. Between levels 25 and 40, only COMMON tier TMs are available,
|
||||||
|
*/
|
||||||
|
function getAndWeightTmMoves(
|
||||||
|
pokemon: Pokemon,
|
||||||
|
currentPool: ReadonlyMap<MoveId, number>,
|
||||||
|
eggPool: ReadonlyMap<MoveId, number>,
|
||||||
|
tmPool: Map<MoveId, number>,
|
||||||
|
): void {
|
||||||
|
const level = pokemon.level;
|
||||||
|
const allowedTiers = getAllowedTmTiers(level);
|
||||||
|
if (!allowedTiers.includes(true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = pokemon.species.forms[pokemon.formIndex]?.formKey ?? "";
|
||||||
|
getTmPoolForSpecies(pokemon.species.speciesId, level, form, currentPool, eggPool, tmPool, allowedTiers);
|
||||||
|
const fusionFormKey = pokemon.getFusionFormKey();
|
||||||
|
const fusionSpecies = pokemon.fusionSpecies?.speciesId;
|
||||||
|
if (fusionSpecies != null && fusionFormKey != null && fusionFormKey !== "") {
|
||||||
|
getTmPoolForSpecies(fusionSpecies, level, fusionFormKey, currentPool, eggPool, tmPool, allowedTiers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the weight multiplier for an egg move
|
||||||
|
* @param levelPool - Map of level up moves to their weights
|
||||||
|
* @param level - The level of the Pokémon
|
||||||
|
* @param forRare - Whether this is for a rare egg move
|
||||||
|
* @param isBoss - Whether the Pokémon having the egg move generated is a boss Pokémon
|
||||||
|
*/
|
||||||
|
export function getEggMoveWeight(
|
||||||
|
// biome-ignore-start lint/correctness/noUnusedFunctionParameters: Saved to allow this algorithm to be tweaked easily without adjusting signatures
|
||||||
|
levelPool: ReadonlyMap<MoveId, number>,
|
||||||
|
level: number,
|
||||||
|
forRare: boolean,
|
||||||
|
isBoss: boolean,
|
||||||
|
// biome-ignore-end lint/correctness/noUnusedFunctionParameters: Endrange
|
||||||
|
): number {
|
||||||
|
const levelUpWeightedEggMoveWeight = Math.round(Math.max(...levelPool.values()) * EGG_MOVE_TO_LEVEL_WEIGHT);
|
||||||
|
// Rare egg moves are always weighted at 5/6 the weight of normal egg moves
|
||||||
|
return Math.min(levelUpWeightedEggMoveWeight, EGG_MOVE_WEIGHT_MAX) * (forRare ? 5 / 6 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submethod of {@linkcode getAndWeightEggMoves} that adds egg moves for a specific species to the egg move pool
|
||||||
|
*
|
||||||
|
* @param rootSpeciesId - The ID of the root species for which to generate the egg move pool.
|
||||||
|
* @param levelPool - A readonly map of move IDs to their levels, representing moves already learned by leveling up.
|
||||||
|
* @param eggPool - A map to be populated with egg move IDs and their corresponding weights.
|
||||||
|
* @param eggMoveWeight - The default weight to assign to regular egg moves.
|
||||||
|
* @param excludeRare - If true, excludes rare egg moves
|
||||||
|
* @param rareEggMoveWeight - The weight to assign to rare egg moves; default 0
|
||||||
|
*
|
||||||
|
* @privateRemarks
|
||||||
|
* Split from `getAndWeightEggMoves` to allow fusion species to add their egg moves without duplicating code.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* - Moves present in `levelPool` are excluded from the egg pool.
|
||||||
|
* - If `excludeRare` is true, rare egg moves (at index 3) are skipped.
|
||||||
|
* - Rare egg moves are assigned `rareEggMoveWeight`, while others receive `eggMoveWeight`.
|
||||||
|
*/
|
||||||
|
function getEggPoolForSpecies(
|
||||||
|
rootSpeciesId: SpeciesId,
|
||||||
|
levelPool: ReadonlyMap<MoveId, number>,
|
||||||
|
eggPool: Map<MoveId, number>,
|
||||||
|
eggMoveWeight: number,
|
||||||
|
excludeRare: boolean,
|
||||||
|
rareEggMoveWeight = 0,
|
||||||
|
): void {
|
||||||
|
const eggMoves = speciesEggMoves[rootSpeciesId];
|
||||||
|
if (eggMoves == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [idx, moveId] of eggMoves.entries()) {
|
||||||
|
if (levelPool.has(moveId) || (idx === 3 && excludeRare)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
eggPool.set(Math.max(moveId, eggPool.get(moveId) ?? 0), idx === 3 ? rareEggMoveWeight : eggMoveWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute and assign a weight to the egg moves currently available to the Pokémon
|
||||||
|
* @param pokemon - The Pokémon to generate egg moves for
|
||||||
|
* @param levelPool - The map of level-based moves to their weights
|
||||||
|
* @param eggPool - A map of move IDs to their weights for egg moves that will be modified in place
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This function checks if the Pokémon meets the requirements to learn egg moves,
|
||||||
|
* and if allowed, calculates the weights for regular and rare egg moves using the provided pools.
|
||||||
|
*/
|
||||||
|
function getAndWeightEggMoves(
|
||||||
|
pokemon: Pokemon,
|
||||||
|
levelPool: ReadonlyMap<MoveId, number>,
|
||||||
|
eggPool: Map<MoveId, number>,
|
||||||
|
): void {
|
||||||
|
const level = pokemon.level;
|
||||||
|
if (level < EGG_MOVE_LEVEL_REQUIREMENT || !globalScene.currentBattle?.trainer?.config.allowEggMoves) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isBoss = pokemon.isBoss();
|
||||||
|
const excludeRare = isBoss || level < RARE_EGG_MOVE_LEVEL_REQUIREMENT;
|
||||||
|
const eggMoveWeight = getEggMoveWeight(levelPool, level, false, isBoss);
|
||||||
|
let rareEggMoveWeight: number | undefined;
|
||||||
|
if (!excludeRare) {
|
||||||
|
rareEggMoveWeight = getEggMoveWeight(levelPool, level, true, isBoss);
|
||||||
|
}
|
||||||
|
getEggPoolForSpecies(
|
||||||
|
pokemon.species.getRootSpeciesId(),
|
||||||
|
levelPool,
|
||||||
|
eggPool,
|
||||||
|
eggMoveWeight,
|
||||||
|
excludeRare,
|
||||||
|
rareEggMoveWeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fusionSpecies = pokemon.fusionSpecies?.getRootSpeciesId();
|
||||||
|
if (fusionSpecies != null) {
|
||||||
|
getEggPoolForSpecies(fusionSpecies, levelPool, eggPool, eggMoveWeight, excludeRare, rareEggMoveWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter a move pool, removing moves that are not allowed based on conditions
|
||||||
|
* @param pool - The move pool to filter
|
||||||
|
* @param isBoss - Whether the Pokémon is a boss
|
||||||
|
* @param hasTrainer - Whether the Pokémon has a trainer
|
||||||
|
*/
|
||||||
|
function filterMovePool(pool: Map<MoveId, number>, isBoss: boolean, hasTrainer: boolean): void {
|
||||||
|
for (const [moveId, weight] of pool) {
|
||||||
|
if (weight <= 0) {
|
||||||
|
pool.delete(moveId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const move = allMoves[moveId];
|
||||||
|
// Forbid unimplemented moves
|
||||||
|
if (move.name.endsWith(" (N)")) {
|
||||||
|
pool.delete(moveId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Bosses never get self ko moves or Pain Split
|
||||||
|
if (isBoss && (move.hasAttr("SacrificialAttr") || move.hasAttr("HpSplitAttr"))) {
|
||||||
|
pool.delete(moveId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No one gets Memento or Final Gambit
|
||||||
|
if (move.hasAttr("SacrificialAttrOnHit")) {
|
||||||
|
pool.delete(moveId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trainers never get OHKO moves
|
||||||
|
if (hasTrainer && move.hasAttr("OneHitKOAttr")) {
|
||||||
|
pool.delete(moveId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform Trainer-specific adjustments to move weights in a move pool
|
||||||
|
* @param pool - The move pool to adjust
|
||||||
|
*/
|
||||||
|
function adjustWeightsForTrainer(pool: Map<MoveId, number>): void {
|
||||||
|
for (const [moveId, weight] of pool.entries()) {
|
||||||
|
const move = allMoves[moveId];
|
||||||
|
let adjustedWeight = weight;
|
||||||
|
// Half the weight of self KO moves on trainers
|
||||||
|
adjustedWeight *= move.hasAttr("SacrificialAttr") ? 0.5 : 1;
|
||||||
|
|
||||||
|
// Trainers get a weight bump to stat buffing moves
|
||||||
|
adjustedWeight *= move.getAttrs("StatStageChangeAttr").some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1;
|
||||||
|
|
||||||
|
// Trainers get a weight decrease to multiturn moves
|
||||||
|
adjustedWeight *= !!move.isChargingMove() || !!move.hasAttr("RechargeAttr") ? 0.7 : 1;
|
||||||
|
if (adjustedWeight !== weight) {
|
||||||
|
pool.set(moveId, adjustedWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust weights of damaging moves in a move pool based on their power and category
|
||||||
|
*
|
||||||
|
* @param pool - The move pool to adjust
|
||||||
|
* @param pokemon - The Pokémon for which the moveset is being generated
|
||||||
|
* @param willTera - Whether the Pokémon is expected to Tera (i.e., has instant Tera on a Trainer Pokémon); default `false`
|
||||||
|
* @remarks
|
||||||
|
* Caps max power at 90 to avoid something like hyper beam ruining the stats.
|
||||||
|
* pokemon is a pretty soft weighting factor, although it is scaled with the weight multiplier.
|
||||||
|
*/
|
||||||
|
function adjustDamageMoveWeights(pool: Map<MoveId, number>, pokemon: Pokemon, willTera = false): void {
|
||||||
|
// begin max power at 40 to avoid inflating weights too much when there are only low power moves
|
||||||
|
let maxPower = 40;
|
||||||
|
for (const moveId of pool.keys()) {
|
||||||
|
const move = allMoves[moveId];
|
||||||
|
maxPower = Math.max(maxPower, move.calculateEffectivePower());
|
||||||
|
if (maxPower >= 90) {
|
||||||
|
maxPower = 90;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const atk = pokemon.getStat(Stat.ATK);
|
||||||
|
const spAtk = pokemon.getStat(Stat.SPATK);
|
||||||
|
const lowerStat = Math.min(atk, spAtk);
|
||||||
|
const higherStat = Math.max(atk, spAtk);
|
||||||
|
const worseCategory = atk > spAtk ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL;
|
||||||
|
const statRatio = lowerStat / higherStat;
|
||||||
|
const adjustmentRatio = Math.min(Math.pow(statRatio, 3) * 1.3, 1);
|
||||||
|
|
||||||
|
for (const [moveId, weight] of pool) {
|
||||||
|
const move = allMoves[moveId];
|
||||||
|
let adjustedWeight = weight;
|
||||||
|
if (move.category === MoveCategory.STATUS) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Scale weight based on their ratio to the highest power move, capping at 50% reduction
|
||||||
|
adjustedWeight *= Math.max(Math.min(move.calculateEffectivePower() / maxPower, 1), 0.5);
|
||||||
|
|
||||||
|
// Scale weight based the stat it uses to deal damage, based on the ratio between said stat
|
||||||
|
// and the higher stat
|
||||||
|
if (move.hasAttr("DefAtkAttr")) {
|
||||||
|
const def = pokemon.getStat(Stat.DEF);
|
||||||
|
const defRatio = def / higherStat;
|
||||||
|
const defAdjustRatio = Math.min(Math.pow(defRatio, 3) * 1.3, 1.1);
|
||||||
|
adjustedWeight *= defAdjustRatio;
|
||||||
|
} else if (
|
||||||
|
move.category === worseCategory
|
||||||
|
&& !move.hasAttr("PhotonGeyserCategoryAttr")
|
||||||
|
&& !move.hasAttr("ShellSideArmCategoryAttr")
|
||||||
|
&& !(move.hasAttr("TeraMoveCategoryAttr") && willTera)
|
||||||
|
) {
|
||||||
|
// Raw multiply each move's category by the stat it uses to deal damage
|
||||||
|
// moves that always use the higher offensive stat are left unadjusted
|
||||||
|
adjustedWeight *= adjustmentRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adjustedWeight !== weight) {
|
||||||
|
pool.set(moveId, adjustedWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the total weight of all moves in a move pool
|
||||||
|
* @param pool - The move pool to calculate the total weight for
|
||||||
|
* @returns The total weight of all moves in the pool
|
||||||
|
*/
|
||||||
|
function calculateTotalPoolWeight(pool: Map<MoveId, number>): number {
|
||||||
|
let totalWeight = 0;
|
||||||
|
for (const weight of pool.values()) {
|
||||||
|
totalWeight += weight;
|
||||||
|
}
|
||||||
|
return totalWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter a pool and return a new array of moves that pass the predicate
|
||||||
|
* @param pool - The move pool to filter
|
||||||
|
* @param predicate - The predicate function to determine if a move should be included
|
||||||
|
* @param totalWeight - An output parameter to hold the total weight of the filtered pool. Its value is reset to 0 if provided.
|
||||||
|
* @returns An array of move ID and weight tuples that pass the predicate
|
||||||
|
*/
|
||||||
|
function filterPool(
|
||||||
|
pool: ReadonlyMap<MoveId, number>,
|
||||||
|
predicate: (moveId: MoveId) => boolean,
|
||||||
|
totalWeight?: NumberHolder,
|
||||||
|
): [id: MoveId, weight: number][] {
|
||||||
|
let hasTotalWeight = false;
|
||||||
|
if (totalWeight != null) {
|
||||||
|
totalWeight.value = 0;
|
||||||
|
hasTotalWeight = true;
|
||||||
|
}
|
||||||
|
const newPool: [id: MoveId, weight: number][] = [];
|
||||||
|
for (const [moveId, weight] of pool) {
|
||||||
|
if (predicate(moveId)) {
|
||||||
|
newPool.push([moveId, weight]);
|
||||||
|
if (hasTotalWeight) {
|
||||||
|
// Bang is safe here because we set `hasTotalWeight` in the if check above
|
||||||
|
totalWeight!.value += weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcibly add a STAB move to the Pokémon's moveset from the provided pools
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* If no STAB move is available, add any damaging move.
|
||||||
|
* If no damaging move is available, no move is added
|
||||||
|
* @param pool - The master move pool
|
||||||
|
* @param tmPool - The TM move pool
|
||||||
|
* @param eggPool - The egg move pool
|
||||||
|
* @param pokemon - The Pokémon for which the moveset is being generated
|
||||||
|
* @param tmCount - A holder for the count of TM moves selected
|
||||||
|
* @param eggMoveCount - A holder for the count of egg moves selected
|
||||||
|
* @param willTera - Whether the Pokémon is expected to Tera (i.e., has instant Tera on a Trainer Pokémon); default `false`
|
||||||
|
* @param forceAnyDamageIfNoStab - If true, will force any damaging move if no STAB move is available
|
||||||
|
*/
|
||||||
|
// biome-ignore lint/nursery/useMaxParams: This is a complex function that needs all these parameters
|
||||||
|
function forceStabMove(
|
||||||
|
pool: Map<MoveId, number>,
|
||||||
|
tmPool: Map<MoveId, number>,
|
||||||
|
eggPool: Map<MoveId, number>,
|
||||||
|
pokemon: Pokemon,
|
||||||
|
tmCount: NumberHolder,
|
||||||
|
eggMoveCount: NumberHolder,
|
||||||
|
willTera = false,
|
||||||
|
forceAnyDamageIfNoStab = false,
|
||||||
|
): void {
|
||||||
|
// All Pokemon force a STAB move first
|
||||||
|
const totalWeight = new NumberHolder(0);
|
||||||
|
const stabMovePool = filterPool(
|
||||||
|
pool,
|
||||||
|
moveId => {
|
||||||
|
const move = allMoves[moveId];
|
||||||
|
return (
|
||||||
|
move.category !== MoveCategory.STATUS
|
||||||
|
&& (pokemon.isOfType(move.type)
|
||||||
|
|| (willTera && move.hasAttr("TeraBlastTypeAttr") && pokemon.getTeraType() !== PokemonType.STELLAR))
|
||||||
|
&& !STAB_BLACKLIST.has(moveId)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
totalWeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
const chosenPool =
|
||||||
|
stabMovePool.length > 0 || !forceAnyDamageIfNoStab
|
||||||
|
? stabMovePool
|
||||||
|
: filterPool(
|
||||||
|
pool,
|
||||||
|
m => allMoves[m[0]].category !== MoveCategory.STATUS && !STAB_BLACKLIST.has(m[0]),
|
||||||
|
totalWeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (chosenPool.length > 0) {
|
||||||
|
let rand = randSeedInt(totalWeight.value);
|
||||||
|
let index = 0;
|
||||||
|
while (rand > chosenPool[index][1]) {
|
||||||
|
rand -= chosenPool[index++][1];
|
||||||
|
}
|
||||||
|
const selectedId = chosenPool[index][0];
|
||||||
|
pool.delete(selectedId);
|
||||||
|
if (tmPool.has(selectedId)) {
|
||||||
|
tmPool.delete(selectedId);
|
||||||
|
tmCount.value++;
|
||||||
|
} else if (eggPool.has(selectedId)) {
|
||||||
|
eggPool.delete(selectedId);
|
||||||
|
eggMoveCount.value++;
|
||||||
|
}
|
||||||
|
pokemon.moveset.push(new PokemonMove(selectedId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust weights in the remaining move pool based on existing moves in the Pokémon's moveset
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* Submethod for step 5 of moveset generation
|
||||||
|
* @param pool - The move pool to filter
|
||||||
|
* @param pokemon - The Pokémon for which the moveset is being generated
|
||||||
|
*/
|
||||||
|
function filterRemainingTrainerMovePool(pool: [id: MoveId, weight: number][], pokemon: Pokemon) {
|
||||||
|
// Sqrt the weight of any damaging moves with overlapping types. pokemon is about a 0.05 - 0.1 multiplier.
|
||||||
|
// Other damaging moves 2x weight if 0-1 damaging moves, 0.5x if 2, 0.125x if 3. These weights get 20x if STAB.
|
||||||
|
// Status moves remain unchanged on weight, pokemon encourages 1-2
|
||||||
|
for (const [idx, [moveId, weight]] of pool.entries()) {
|
||||||
|
let ret: number;
|
||||||
|
if (
|
||||||
|
pokemon.moveset.some(
|
||||||
|
mo => mo.getMove().category !== MoveCategory.STATUS && mo.getMove().type === allMoves[moveId].type,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ret = Math.ceil(Math.sqrt(weight));
|
||||||
|
} else if (allMoves[moveId].category !== MoveCategory.STATUS) {
|
||||||
|
ret = Math.ceil(
|
||||||
|
(weight / Math.max(Math.pow(4, pokemon.moveset.filter(mo => (mo.getMove().power ?? 0) > 1).length) / 8, 0.5))
|
||||||
|
* (pokemon.isOfType(allMoves[moveId].type) && !STAB_BLACKLIST.has(moveId) ? 20 : 1),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ret = weight;
|
||||||
|
}
|
||||||
|
pool[idx] = [moveId, ret];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill in the remaining slots in the Pokémon's moveset from the provided pools
|
||||||
|
* @param pokemon - The Pokémon for which the moveset is being generated
|
||||||
|
* @param tmPool - The TM move pool
|
||||||
|
* @param eggMovePool - The egg move pool
|
||||||
|
* @param tmCount - A holder for the count of moves that have been added to the moveset from TMs
|
||||||
|
* @param eggMoveCount - A holder for the count of moves that have been added to the moveset from egg moves
|
||||||
|
* @param baseWeights - The base weights of all moves in the master pool
|
||||||
|
* @param remainingPool - The remaining move pool to select from
|
||||||
|
*/
|
||||||
|
function fillInRemainingMovesetSlots(
|
||||||
|
pokemon: Pokemon,
|
||||||
|
tmPool: Map<MoveId, number>,
|
||||||
|
eggMovePool: Map<MoveId, number>,
|
||||||
|
tmCount: NumberHolder,
|
||||||
|
eggMoveCount: NumberHolder,
|
||||||
|
baseWeights: Map<MoveId, number>,
|
||||||
|
remainingPool: [id: MoveId, weight: number][],
|
||||||
|
): void {
|
||||||
|
const tmCap = getMaxTmCount(pokemon.level);
|
||||||
|
const eggCap = getMaxEggMoveCount(pokemon.level);
|
||||||
|
const remainingPoolWeight = new NumberHolder(0);
|
||||||
|
while (remainingPool.length > pokemon.moveset.length && pokemon.moveset.length < 4) {
|
||||||
|
const nonLevelMoveCount = tmCount.value + eggMoveCount.value;
|
||||||
|
remainingPool = filterPool(
|
||||||
|
baseWeights,
|
||||||
|
(m: MoveId) =>
|
||||||
|
!pokemon.moveset.some(
|
||||||
|
mo =>
|
||||||
|
m === mo.moveId || (allMoves[m]?.hasAttr("SacrificialAttr") && mo.getMove()?.hasAttr("SacrificialAttr")), // Only one self-KO move allowed
|
||||||
|
)
|
||||||
|
&& (nonLevelMoveCount < tmCap || !tmPool.has(m))
|
||||||
|
&& (nonLevelMoveCount < eggCap || !eggMovePool.has(m)),
|
||||||
|
remainingPoolWeight,
|
||||||
|
);
|
||||||
|
if (pokemon.hasTrainer()) {
|
||||||
|
filterRemainingTrainerMovePool(remainingPool, pokemon);
|
||||||
|
}
|
||||||
|
const totalWeight = remainingPool.reduce((v, m) => v + m[1], 0);
|
||||||
|
let rand = randSeedInt(totalWeight);
|
||||||
|
let index = 0;
|
||||||
|
while (rand > remainingPool[index][1]) {
|
||||||
|
rand -= remainingPool[index++][1];
|
||||||
|
}
|
||||||
|
const selectedMoveId = remainingPool[index][0];
|
||||||
|
baseWeights.delete(selectedMoveId);
|
||||||
|
if (tmPool.has(selectedMoveId)) {
|
||||||
|
tmCount.value++;
|
||||||
|
tmPool.delete(selectedMoveId);
|
||||||
|
} else if (eggMovePool.has(selectedMoveId)) {
|
||||||
|
eggMoveCount.value++;
|
||||||
|
eggMovePool.delete(selectedMoveId);
|
||||||
|
}
|
||||||
|
pokemon.moveset.push(new PokemonMove(selectedMoveId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debugging function to log computed move weights for a Pokémon
|
||||||
|
* @param pokemon - The Pokémon for which the move weights were computed
|
||||||
|
* @param pool - The move pool containing move IDs and their weights
|
||||||
|
* @param note - Short note to include in the log for context
|
||||||
|
*/
|
||||||
|
function debugMoveWeights(pokemon: Pokemon, pool: Map<MoveId, number>, note: string): void {
|
||||||
|
if ((isBeta || import.meta.env.DEV) && import.meta.env.NODE_ENV !== "test") {
|
||||||
|
const moveNameToWeightMap = new Map<string, number>();
|
||||||
|
const sortedByValue = Array.from(pool.entries()).sort((a, b) => b[1] - a[1]);
|
||||||
|
for (const [moveId, weight] of sortedByValue) {
|
||||||
|
moveNameToWeightMap.set(allMoves[moveId].name, weight);
|
||||||
|
}
|
||||||
|
console.log("%cComputed move weights [%s] for %s", "color: blue", note, pokemon.name, moveNameToWeightMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a moveset for a given Pokémon based on its level, types, stats, and whether it is wild or a trainer's Pokémon.
|
||||||
|
* @param pokemon - The Pokémon to generate a moveset for
|
||||||
|
* @returns A reference to the Pokémon's moveset array
|
||||||
|
*/
|
||||||
|
export function generateMoveset(pokemon: Pokemon): void {
|
||||||
|
pokemon.moveset = [];
|
||||||
|
// Step 1: Generate the pools from various sources: level up, egg moves, and TMs
|
||||||
|
const learnPool = getAndWeightLevelMoves(pokemon);
|
||||||
|
debugMoveWeights(pokemon, learnPool, "Initial Level Moves");
|
||||||
|
const hasTrainer = pokemon.hasTrainer();
|
||||||
|
const tmPool = new Map<MoveId, number>();
|
||||||
|
const eggMovePool = new Map<MoveId, number>();
|
||||||
|
|
||||||
|
if (hasTrainer) {
|
||||||
|
getAndWeightEggMoves(pokemon, learnPool, eggMovePool);
|
||||||
|
eggMovePool.size > 0 && debugMoveWeights(pokemon, eggMovePool, "Initial Egg Moves");
|
||||||
|
getAndWeightTmMoves(pokemon, learnPool, eggMovePool, tmPool);
|
||||||
|
tmPool.size > 0 && debugMoveWeights(pokemon, tmPool, "Initial Tm Moves");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, combine pools into one master pool.
|
||||||
|
// The pools are kept around so we know where the move was sourced from
|
||||||
|
const movePool = new Map<MoveId, number>([...tmPool.entries(), ...eggMovePool.entries(), ...learnPool.entries()]);
|
||||||
|
|
||||||
|
// Step 2: Filter out forbidden moves
|
||||||
|
const isBoss = pokemon.isBoss();
|
||||||
|
filterMovePool(movePool, isBoss, hasTrainer);
|
||||||
|
|
||||||
|
// Step 3: Adjust weights for trainers
|
||||||
|
if (hasTrainer) {
|
||||||
|
adjustWeightsForTrainer(movePool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Determine whether this pokemon will instantly tera */
|
||||||
|
const willTera =
|
||||||
|
hasTrainer
|
||||||
|
&& globalScene.currentBattle?.trainer?.config.trainerAI.instantTeras.includes(
|
||||||
|
// The cast to EnemyPokemon is safe; includes will just return false if the property doesn't exist
|
||||||
|
(pokemon as EnemyPokemon).initialTeamIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
adjustDamageMoveWeights(movePool, pokemon, willTera);
|
||||||
|
|
||||||
|
/** The higher this is, the greater the impact of weight. At `0` all moves are equal weight. */
|
||||||
|
let weightMultiplier = BASE_WEIGHT_MULTIPLIER;
|
||||||
|
if (isBoss) {
|
||||||
|
weightMultiplier += BOSS_EXTRA_WEIGHT_MULTIPLIER;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseWeights = new Map<MoveId, number>(movePool);
|
||||||
|
for (const [moveId, weight] of baseWeights) {
|
||||||
|
if (weight <= 0) {
|
||||||
|
baseWeights.delete(moveId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
baseWeights.set(moveId, Math.ceil(Math.pow(weight, weightMultiplier) * 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmCount = new NumberHolder(0);
|
||||||
|
const eggMoveCount = new NumberHolder(0);
|
||||||
|
|
||||||
|
debugMoveWeights(pokemon, baseWeights, "Pre STAB Move");
|
||||||
|
|
||||||
|
// Step 4: Force a STAB move if possible
|
||||||
|
forceStabMove(movePool, tmPool, eggMovePool, pokemon, tmCount, eggMoveCount, willTera);
|
||||||
|
// Note: To force a secondary stab, call this a second time, and pass `false` for the last parameter
|
||||||
|
// Would also tweak the function to not consider moves already in the moveset
|
||||||
|
// e.g. forceStabMove(..., false);
|
||||||
|
|
||||||
|
// Step 5: Fill in remaining slots
|
||||||
|
fillInRemainingMovesetSlots(
|
||||||
|
pokemon,
|
||||||
|
tmPool,
|
||||||
|
eggMovePool,
|
||||||
|
tmCount,
|
||||||
|
eggMoveCount,
|
||||||
|
baseWeights,
|
||||||
|
filterPool(baseWeights, (m: MoveId) => !pokemon.moveset.some(mo => m[0] === mo.moveId)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports for internal testing purposes.
|
||||||
|
* ⚠️ These *must not* be used outside of tests, as they will not be defined.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const __INTERNAL_TEST_EXPORTS: {
|
||||||
|
getAndWeightLevelMoves: typeof getAndWeightLevelMoves;
|
||||||
|
getAllowedTmTiers: typeof getAllowedTmTiers;
|
||||||
|
getTmPoolForSpecies: typeof getTmPoolForSpecies;
|
||||||
|
getAndWeightTmMoves: typeof getAndWeightTmMoves;
|
||||||
|
getEggMoveWeight: typeof getEggMoveWeight;
|
||||||
|
getEggPoolForSpecies: typeof getEggPoolForSpecies;
|
||||||
|
getAndWeightEggMoves: typeof getAndWeightEggMoves;
|
||||||
|
filterMovePool: typeof filterMovePool;
|
||||||
|
adjustWeightsForTrainer: typeof adjustWeightsForTrainer;
|
||||||
|
adjustDamageMoveWeights: typeof adjustDamageMoveWeights;
|
||||||
|
calculateTotalPoolWeight: typeof calculateTotalPoolWeight;
|
||||||
|
filterPool: typeof filterPool;
|
||||||
|
forceStabMove: typeof forceStabMove;
|
||||||
|
filterRemainingTrainerMovePool: typeof filterRemainingTrainerMovePool;
|
||||||
|
fillInRemainingMovesetSlots: typeof fillInRemainingMovesetSlots;
|
||||||
|
} = {} as any;
|
||||||
|
|
||||||
|
// We can't use `import.meta.vitest` here, because this would not be set
|
||||||
|
// until the tests themselves begin to run, which is after imports
|
||||||
|
// So we rely on NODE_ENV being test instead
|
||||||
|
if (import.meta.env.NODE_ENV === "test") {
|
||||||
|
Object.assign(__INTERNAL_TEST_EXPORTS, {
|
||||||
|
getAndWeightLevelMoves,
|
||||||
|
getAllowedTmTiers,
|
||||||
|
getTmPoolForSpecies,
|
||||||
|
getAndWeightTmMoves,
|
||||||
|
getEggMoveWeight,
|
||||||
|
getEggPoolForSpecies,
|
||||||
|
getAndWeightEggMoves,
|
||||||
|
filterMovePool,
|
||||||
|
adjustWeightsForTrainer,
|
||||||
|
adjustDamageMoveWeights,
|
||||||
|
calculateTotalPoolWeight,
|
||||||
|
filterPool,
|
||||||
|
forceStabMove,
|
||||||
|
filterRemainingTrainerMovePool,
|
||||||
|
fillInRemainingMovesetSlots,
|
||||||
|
});
|
||||||
|
}
|
@ -138,7 +138,6 @@ import {
|
|||||||
formatMoney,
|
formatMoney,
|
||||||
getIvsFromId,
|
getIvsFromId,
|
||||||
isBetween,
|
isBetween,
|
||||||
isNullOrUndefined,
|
|
||||||
NumberHolder,
|
NumberHolder,
|
||||||
randomString,
|
randomString,
|
||||||
randSeedInt,
|
randSeedInt,
|
||||||
@ -859,20 +858,21 @@ export class BattleScene extends SceneBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the {@linkcode Pokemon} associated with a given ID.
|
* Return the {@linkcode Pokemon} associated with the given ID.
|
||||||
* @param pokemonId - The ID whose Pokemon will be retrieved.
|
* @param pokemonId - The PID whose Pokemon will be retrieved
|
||||||
* @returns The {@linkcode Pokemon} associated with the given id.
|
* @returns The `Pokemon` associated with the given ID,
|
||||||
* Returns `null` if the ID is `undefined` or not present in either party.
|
* or `undefined` if none is found in either team's party.
|
||||||
* @todo Change the `null` to `undefined` and update callers' signatures -
|
* @see {@linkcode Pokemon.id}
|
||||||
* this is weird and causes a lot of random jank
|
* @todo `pokemonId` should not allow `undefined`
|
||||||
*/
|
*/
|
||||||
getPokemonById(pokemonId: number | undefined): Pokemon | null {
|
public getPokemonById(pokemonId: number | undefined): Pokemon | undefined {
|
||||||
if (isNullOrUndefined(pokemonId)) {
|
if (pokemonId == null) {
|
||||||
return null;
|
// biome-ignore lint/nursery/noUselessUndefined: More explicit
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const party = (this.getPlayerParty() as Pokemon[]).concat(this.getEnemyParty());
|
const party = (this.getPlayerParty() as Pokemon[]).concat(this.getEnemyParty());
|
||||||
return party.find(p => p.id === pokemonId) ?? null;
|
return party.find(p => p.id === pokemonId);
|
||||||
}
|
}
|
||||||
|
|
||||||
addPlayerPokemon(
|
addPlayerPokemon(
|
||||||
@ -1319,7 +1319,7 @@ export class BattleScene extends SceneBase {
|
|||||||
if (
|
if (
|
||||||
!this.gameMode.hasTrainers
|
!this.gameMode.hasTrainers
|
||||||
|| Overrides.BATTLE_TYPE_OVERRIDE === BattleType.WILD
|
|| Overrides.BATTLE_TYPE_OVERRIDE === BattleType.WILD
|
||||||
|| (Overrides.DISABLE_STANDARD_TRAINERS_OVERRIDE && isNullOrUndefined(trainerData))
|
|| (Overrides.DISABLE_STANDARD_TRAINERS_OVERRIDE && trainerData == null)
|
||||||
) {
|
) {
|
||||||
newBattleType = BattleType.WILD;
|
newBattleType = BattleType.WILD;
|
||||||
} else {
|
} else {
|
||||||
@ -1332,13 +1332,12 @@ export class BattleScene extends SceneBase {
|
|||||||
if (newBattleType === BattleType.TRAINER) {
|
if (newBattleType === BattleType.TRAINER) {
|
||||||
const trainerType =
|
const trainerType =
|
||||||
Overrides.RANDOM_TRAINER_OVERRIDE?.trainerType ?? this.arena.randomTrainerType(newWaveIndex);
|
Overrides.RANDOM_TRAINER_OVERRIDE?.trainerType ?? this.arena.randomTrainerType(newWaveIndex);
|
||||||
|
const hasDouble = trainerConfigs[trainerType].hasDouble;
|
||||||
let doubleTrainer = false;
|
let doubleTrainer = false;
|
||||||
if (trainerConfigs[trainerType].doubleOnly) {
|
if (trainerConfigs[trainerType].doubleOnly) {
|
||||||
doubleTrainer = true;
|
doubleTrainer = true;
|
||||||
} else if (trainerConfigs[trainerType].hasDouble) {
|
} else if (hasDouble) {
|
||||||
doubleTrainer =
|
doubleTrainer = !randSeedInt(this.getDoubleBattleChance(newWaveIndex, playerField));
|
||||||
Overrides.RANDOM_TRAINER_OVERRIDE?.alwaysDouble
|
|
||||||
|| !randSeedInt(this.getDoubleBattleChance(newWaveIndex, playerField));
|
|
||||||
// Add a check that special trainers can't be double except for tate and liza - they should use the normal double chance
|
// Add a check that special trainers can't be double except for tate and liza - they should use the normal double chance
|
||||||
if (
|
if (
|
||||||
trainerConfigs[trainerType].trainerTypeDouble
|
trainerConfigs[trainerType].trainerTypeDouble
|
||||||
@ -1347,11 +1346,19 @@ export class BattleScene extends SceneBase {
|
|||||||
doubleTrainer = false;
|
doubleTrainer = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const variant = doubleTrainer
|
|
||||||
? TrainerVariant.DOUBLE
|
// Forcing a double battle on wave 1 causes a bug where only one enemy is sent out,
|
||||||
: randSeedInt(2)
|
// making it impossible to complete the fight without a reload
|
||||||
? TrainerVariant.FEMALE
|
const overrideVariant =
|
||||||
: TrainerVariant.DEFAULT;
|
Overrides.RANDOM_TRAINER_OVERRIDE?.trainerVariant === TrainerVariant.DOUBLE
|
||||||
|
&& (!hasDouble || newWaveIndex <= 1)
|
||||||
|
? TrainerVariant.DEFAULT
|
||||||
|
: Overrides.RANDOM_TRAINER_OVERRIDE?.trainerVariant;
|
||||||
|
|
||||||
|
const variant =
|
||||||
|
overrideVariant
|
||||||
|
?? (doubleTrainer ? TrainerVariant.DOUBLE : randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT);
|
||||||
|
|
||||||
newTrainer = trainerData !== undefined ? trainerData.toTrainer() : new Trainer(trainerType, variant);
|
newTrainer = trainerData !== undefined ? trainerData.toTrainer() : new Trainer(trainerType, variant);
|
||||||
this.field.add(newTrainer);
|
this.field.add(newTrainer);
|
||||||
}
|
}
|
||||||
@ -1383,7 +1390,7 @@ export class BattleScene extends SceneBase {
|
|||||||
newDouble = false;
|
newDouble = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isNullOrUndefined(Overrides.BATTLE_STYLE_OVERRIDE)) {
|
if (Overrides.BATTLE_STYLE_OVERRIDE != null) {
|
||||||
let doubleOverrideForWave: "single" | "double" | null = null;
|
let doubleOverrideForWave: "single" | "double" | null = null;
|
||||||
|
|
||||||
switch (Overrides.BATTLE_STYLE_OVERRIDE) {
|
switch (Overrides.BATTLE_STYLE_OVERRIDE) {
|
||||||
@ -1572,7 +1579,7 @@ export class BattleScene extends SceneBase {
|
|||||||
// Give trainers with specialty types an appropriately-typed form for Wormadam, Rotom, Arceus, Oricorio, Silvally, or Paldean Tauros.
|
// Give trainers with specialty types an appropriately-typed form for Wormadam, Rotom, Arceus, Oricorio, Silvally, or Paldean Tauros.
|
||||||
!isEggPhase
|
!isEggPhase
|
||||||
&& this.currentBattle?.battleType === BattleType.TRAINER
|
&& this.currentBattle?.battleType === BattleType.TRAINER
|
||||||
&& !isNullOrUndefined(this.currentBattle.trainer)
|
&& this.currentBattle.trainer != null
|
||||||
&& this.currentBattle.trainer.config.hasSpecialtyType()
|
&& this.currentBattle.trainer.config.hasSpecialtyType()
|
||||||
) {
|
) {
|
||||||
if (species.speciesId === SpeciesId.WORMADAM) {
|
if (species.speciesId === SpeciesId.WORMADAM) {
|
||||||
@ -2692,7 +2699,7 @@ export class BattleScene extends SceneBase {
|
|||||||
}
|
}
|
||||||
} else if (modifier instanceof FusePokemonModifier) {
|
} else if (modifier instanceof FusePokemonModifier) {
|
||||||
args.push(this.getPokemonById(modifier.fusePokemonId) as PlayerPokemon);
|
args.push(this.getPokemonById(modifier.fusePokemonId) as PlayerPokemon);
|
||||||
} else if (modifier instanceof RememberMoveModifier && !isNullOrUndefined(cost)) {
|
} else if (modifier instanceof RememberMoveModifier && cost != null) {
|
||||||
args.push(cost);
|
args.push(cost);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3007,7 +3014,7 @@ export class BattleScene extends SceneBase {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
modifier instanceof PokemonHeldItemModifier
|
modifier instanceof PokemonHeldItemModifier
|
||||||
&& !isNullOrUndefined(modifier.getSpecies())
|
&& modifier.getSpecies() != null
|
||||||
&& !this.getPokemonById(modifier.pokemonId)?.hasSpecies(modifier.getSpecies()!)
|
&& !this.getPokemonById(modifier.pokemonId)?.hasSpecies(modifier.getSpecies()!)
|
||||||
) {
|
) {
|
||||||
modifiers.splice(m--, 1);
|
modifiers.splice(m--, 1);
|
||||||
@ -3573,7 +3580,7 @@ export class BattleScene extends SceneBase {
|
|||||||
// Loading override or session encounter
|
// Loading override or session encounter
|
||||||
let encounter: MysteryEncounter | null;
|
let encounter: MysteryEncounter | null;
|
||||||
if (
|
if (
|
||||||
!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_OVERRIDE)
|
Overrides.MYSTERY_ENCOUNTER_OVERRIDE != null
|
||||||
&& allMysteryEncounters.hasOwnProperty(Overrides.MYSTERY_ENCOUNTER_OVERRIDE)
|
&& allMysteryEncounters.hasOwnProperty(Overrides.MYSTERY_ENCOUNTER_OVERRIDE)
|
||||||
) {
|
) {
|
||||||
encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE];
|
encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE];
|
||||||
@ -3584,7 +3591,7 @@ export class BattleScene extends SceneBase {
|
|||||||
encounter = allMysteryEncounters[encounterType ?? -1];
|
encounter = allMysteryEncounters[encounterType ?? -1];
|
||||||
return encounter;
|
return encounter;
|
||||||
} else {
|
} else {
|
||||||
encounter = !isNullOrUndefined(encounterType) ? allMysteryEncounters[encounterType] : null;
|
encounter = encounterType != null ? allMysteryEncounters[encounterType] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for queued encounters first
|
// Check for queued encounters first
|
||||||
@ -3643,7 +3650,7 @@ export class BattleScene extends SceneBase {
|
|||||||
? MysteryEncounterTier.ULTRA
|
? MysteryEncounterTier.ULTRA
|
||||||
: MysteryEncounterTier.ROGUE;
|
: MysteryEncounterTier.ROGUE;
|
||||||
|
|
||||||
if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE)) {
|
if (Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE != null) {
|
||||||
tier = Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE;
|
tier = Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +74,10 @@ export class Battle {
|
|||||||
public battleSeed: string = randomString(16, true);
|
public battleSeed: string = randomString(16, true);
|
||||||
private battleSeedState: string | null = null;
|
private battleSeedState: string | null = null;
|
||||||
public moneyScattered = 0;
|
public moneyScattered = 0;
|
||||||
/** Primarily for double battles, keeps track of last enemy and player pokemon that triggered its ability or used a move */
|
/**
|
||||||
|
* Primarily for double battles, keeps track of last enemy and player pokemon that triggered its ability or used a move
|
||||||
|
* @todo THis is only used for Sticky Web and in a really jank way...
|
||||||
|
*/
|
||||||
public lastEnemyInvolved: number;
|
public lastEnemyInvolved: number;
|
||||||
public lastPlayerInvolved: number;
|
public lastPlayerInvolved: number;
|
||||||
public lastUsedPokeball: PokeballType | null = null;
|
public lastUsedPokeball: PokeballType | null = null;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* @module
|
*
|
||||||
* A big file storing colors used in logging.
|
* A big file storing colors used in logging.
|
||||||
* Minified by Terser during production builds, so has no overhead.
|
* Minified by Terser during production builds, so has no overhead.
|
||||||
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Colors used in prod
|
// Colors used in prod
|
||||||
|
@ -70,7 +70,6 @@ import type { Constructor } from "#utils/common";
|
|||||||
import {
|
import {
|
||||||
BooleanHolder,
|
BooleanHolder,
|
||||||
coerceArray,
|
coerceArray,
|
||||||
isNullOrUndefined,
|
|
||||||
NumberHolder,
|
NumberHolder,
|
||||||
randSeedFloat,
|
randSeedFloat,
|
||||||
randSeedInt,
|
randSeedInt,
|
||||||
@ -1043,7 +1042,7 @@ export class PostDefendStatStageChangeAbAttr extends PostDefendAbAttr {
|
|||||||
|
|
||||||
if (this.allOthers) {
|
if (this.allOthers) {
|
||||||
const ally = pokemon.getAlly();
|
const ally = pokemon.getAlly();
|
||||||
const otherPokemon = !isNullOrUndefined(ally) ? pokemon.getOpponents().concat([ally]) : pokemon.getOpponents();
|
const otherPokemon = ally != null ? pokemon.getOpponents().concat([ally]) : pokemon.getOpponents();
|
||||||
for (const other of otherPokemon) {
|
for (const other of otherPokemon) {
|
||||||
globalScene.phaseManager.unshiftNew(
|
globalScene.phaseManager.unshiftNew(
|
||||||
"StatStageChangePhase",
|
"StatStageChangePhase",
|
||||||
@ -1476,7 +1475,7 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr {
|
|||||||
|
|
||||||
override canApply({ move, opponent: attacker, pokemon }: PostMoveInteractionAbAttrParams): boolean {
|
override canApply({ move, opponent: attacker, pokemon }: PostMoveInteractionAbAttrParams): boolean {
|
||||||
return (
|
return (
|
||||||
isNullOrUndefined(attacker.getTag(BattlerTagType.DISABLED))
|
attacker.getTag(BattlerTagType.DISABLED) == null
|
||||||
&& move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon })
|
&& move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon })
|
||||||
&& (this.chance === -1 || pokemon.randBattleSeedInt(100) < this.chance)
|
&& (this.chance === -1 || pokemon.randBattleSeedInt(100) < this.chance)
|
||||||
);
|
);
|
||||||
@ -2813,7 +2812,7 @@ export class PostSummonAllyHealAbAttr extends PostSummonAbAttr {
|
|||||||
|
|
||||||
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
|
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
|
||||||
const target = pokemon.getAlly();
|
const target = pokemon.getAlly();
|
||||||
if (!simulated && !isNullOrUndefined(target)) {
|
if (!simulated && target != null) {
|
||||||
globalScene.phaseManager.unshiftNew(
|
globalScene.phaseManager.unshiftNew(
|
||||||
"PokemonHealPhase",
|
"PokemonHealPhase",
|
||||||
target.getBattlerIndex(),
|
target.getBattlerIndex(),
|
||||||
@ -2844,7 +2843,7 @@ export class PostSummonClearAllyStatStagesAbAttr extends PostSummonAbAttr {
|
|||||||
|
|
||||||
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
|
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
|
||||||
const target = pokemon.getAlly();
|
const target = pokemon.getAlly();
|
||||||
if (!simulated && !isNullOrUndefined(target)) {
|
if (!simulated && target != null) {
|
||||||
for (const s of BATTLE_STATS) {
|
for (const s of BATTLE_STATS) {
|
||||||
target.setStatStage(s, 0);
|
target.setStatStage(s, 0);
|
||||||
}
|
}
|
||||||
@ -2963,13 +2962,13 @@ export class PostSummonHealStatusAbAttr extends PostSummonRemoveEffectAbAttr {
|
|||||||
|
|
||||||
public override canApply({ pokemon }: AbAttrBaseParams): boolean {
|
public override canApply({ pokemon }: AbAttrBaseParams): boolean {
|
||||||
const status = pokemon.status?.effect;
|
const status = pokemon.status?.effect;
|
||||||
return !isNullOrUndefined(status) && (this.immuneEffects.length === 0 || this.immuneEffects.includes(status));
|
return status != null && (this.immuneEffects.length === 0 || this.immuneEffects.includes(status));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override apply({ pokemon }: AbAttrBaseParams): void {
|
public override apply({ pokemon }: AbAttrBaseParams): void {
|
||||||
// TODO: should probably check against simulated...
|
// TODO: should probably check against simulated...
|
||||||
const status = pokemon.status?.effect;
|
const status = pokemon.status?.effect;
|
||||||
if (!isNullOrUndefined(status)) {
|
if (status != null) {
|
||||||
this.statusHealed = status;
|
this.statusHealed = status;
|
||||||
pokemon.resetStatus(false);
|
pokemon.resetStatus(false);
|
||||||
pokemon.updateInfo();
|
pokemon.updateInfo();
|
||||||
@ -3105,7 +3104,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ally = pokemon.getAlly();
|
const ally = pokemon.getAlly();
|
||||||
return !(isNullOrUndefined(ally) || ally.getStatStages().every(s => s === 0));
|
return !(ally == null || ally.getStatStages().every(s => s === 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
|
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
|
||||||
@ -3113,7 +3112,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ally = pokemon.getAlly();
|
const ally = pokemon.getAlly();
|
||||||
if (!isNullOrUndefined(ally)) {
|
if (ally != null) {
|
||||||
for (const s of BATTLE_STATS) {
|
for (const s of BATTLE_STATS) {
|
||||||
pokemon.setStatStage(s, ally.getStatStage(s));
|
pokemon.setStatStage(s, ally.getStatStage(s));
|
||||||
}
|
}
|
||||||
@ -3243,7 +3242,7 @@ export class CommanderAbAttr extends AbAttr {
|
|||||||
const ally = pokemon.getAlly();
|
const ally = pokemon.getAlly();
|
||||||
return (
|
return (
|
||||||
globalScene.currentBattle?.double
|
globalScene.currentBattle?.double
|
||||||
&& !isNullOrUndefined(ally)
|
&& ally != null
|
||||||
&& ally.species.speciesId === SpeciesId.DONDOZO
|
&& ally.species.speciesId === SpeciesId.DONDOZO
|
||||||
&& !(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED))
|
&& !(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED))
|
||||||
);
|
);
|
||||||
@ -3287,7 +3286,7 @@ export class PreSwitchOutResetStatusAbAttr extends PreSwitchOutAbAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override canApply({ pokemon }: AbAttrBaseParams): boolean {
|
override canApply({ pokemon }: AbAttrBaseParams): boolean {
|
||||||
return !isNullOrUndefined(pokemon.status);
|
return pokemon.status != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
|
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
|
||||||
@ -3567,7 +3566,7 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override canApply({ stat, cancelled }: PreStatStageChangeAbAttrParams): boolean {
|
override canApply({ stat, cancelled }: PreStatStageChangeAbAttrParams): boolean {
|
||||||
return !cancelled.value && (isNullOrUndefined(this.protectedStat) || stat === this.protectedStat);
|
return !cancelled.value && (this.protectedStat == null || stat === this.protectedStat);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3803,11 +3802,7 @@ export class ConditionalUserFieldProtectStatAbAttr extends PreStatStageChangeAbA
|
|||||||
if (!target) {
|
if (!target) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return (
|
return !cancelled.value && (this.protectedStat == null || stat === this.protectedStat) && this.condition(target);
|
||||||
!cancelled.value
|
|
||||||
&& (isNullOrUndefined(this.protectedStat) || stat === this.protectedStat)
|
|
||||||
&& this.condition(target)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -4564,7 +4559,7 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override canApply({ pokemon }: AbAttrBaseParams): boolean {
|
override canApply({ pokemon }: AbAttrBaseParams): boolean {
|
||||||
return !isNullOrUndefined(pokemon.status) && this.effects.includes(pokemon.status.effect) && !pokemon.isFullHp();
|
return pokemon.status != null && this.effects.includes(pokemon.status.effect) && !pokemon.isFullHp();
|
||||||
}
|
}
|
||||||
|
|
||||||
override apply({ simulated, passive, pokemon }: AbAttrBaseParams): void {
|
override apply({ simulated, passive, pokemon }: AbAttrBaseParams): void {
|
||||||
@ -4899,7 +4894,7 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr {
|
|||||||
*/
|
*/
|
||||||
export class FetchBallAbAttr extends PostTurnAbAttr {
|
export class FetchBallAbAttr extends PostTurnAbAttr {
|
||||||
override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean {
|
override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean {
|
||||||
return !simulated && !isNullOrUndefined(globalScene.currentBattle.lastUsedPokeball) && !!pokemon.isPlayer;
|
return !simulated && globalScene.currentBattle.lastUsedPokeball != null && !!pokemon.isPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -6273,7 +6268,7 @@ class ForceSwitchOutHelper {
|
|||||||
true,
|
true,
|
||||||
500,
|
500,
|
||||||
);
|
);
|
||||||
if (globalScene.currentBattle.double && !isNullOrUndefined(allyPokemon)) {
|
if (globalScene.currentBattle.double && allyPokemon != null) {
|
||||||
globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon);
|
globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7125,7 +7120,7 @@ export function initAbilities() {
|
|||||||
.attr(PostDefendMoveDisableAbAttr, 30)
|
.attr(PostDefendMoveDisableAbAttr, 30)
|
||||||
.bypassFaint(),
|
.bypassFaint(),
|
||||||
new Ability(AbilityId.HEALER, 5)
|
new Ability(AbilityId.HEALER, 5)
|
||||||
.conditionalAttr(pokemon => !isNullOrUndefined(pokemon.getAlly()) && randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true),
|
.conditionalAttr(pokemon => pokemon.getAlly() != null && randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true),
|
||||||
new Ability(AbilityId.FRIEND_GUARD, 5)
|
new Ability(AbilityId.FRIEND_GUARD, 5)
|
||||||
.attr(AlliedFieldDamageReductionAbAttr, 0.75)
|
.attr(AlliedFieldDamageReductionAbAttr, 0.75)
|
||||||
.ignorable(),
|
.ignorable(),
|
||||||
|
@ -1,39 +1,4 @@
|
|||||||
/** biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports */
|
|
||||||
import type { BattlerTag } from "#app/data/battler-tags";
|
|
||||||
/** biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports */
|
|
||||||
|
|
||||||
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
|
|
||||||
import { globalScene } from "#app/global-scene";
|
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
|
||||||
import { CommonBattleAnim } from "#data/battle-anims";
|
|
||||||
import { allMoves } from "#data/data-lists";
|
|
||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
|
||||||
import { HitResult } from "#enums/hit-result";
|
|
||||||
import { CommonAnim } from "#enums/move-anims-common";
|
|
||||||
import { MoveCategory } from "#enums/move-category";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { MoveTarget } from "#enums/move-target";
|
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
|
||||||
import { Stat } from "#enums/stat";
|
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
|
||||||
import type { Arena } from "#field/arena";
|
|
||||||
import type { Pokemon } from "#field/pokemon";
|
|
||||||
import type {
|
|
||||||
ArenaScreenTagType,
|
|
||||||
ArenaTagData,
|
|
||||||
EntryHazardTagType,
|
|
||||||
RoomArenaTagType,
|
|
||||||
SerializableArenaTagType,
|
|
||||||
} from "#types/arena-tags";
|
|
||||||
import type { Mutable } from "#types/type-helpers";
|
|
||||||
import { BooleanHolder, type NumberHolder, toDmgValue } from "#utils/common";
|
|
||||||
import i18next from "i18next";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module
|
|
||||||
* ArenaTags are are meant for effects that are tied to the arena (as opposed to a specific pokemon).
|
* ArenaTags are are meant for effects that are tied to the arena (as opposed to a specific pokemon).
|
||||||
* Examples include (but are not limited to)
|
* Examples include (but are not limited to)
|
||||||
* - Cross-turn effects that persist even if the user/target switches out, such as Happy Hour
|
* - Cross-turn effects that persist even if the user/target switches out, such as Happy Hour
|
||||||
@ -76,8 +41,43 @@ import i18next from "i18next";
|
|||||||
* ```
|
* ```
|
||||||
* Notes
|
* Notes
|
||||||
* - If the class has any subclasses, then the second form of `loadTag` *must* be used.
|
* - If the class has any subclasses, then the second form of `loadTag` *must* be used.
|
||||||
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports
|
||||||
|
import type { BattlerTag } from "#app/data/battler-tags";
|
||||||
|
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports
|
||||||
|
|
||||||
|
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
|
||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import { CommonBattleAnim } from "#data/battle-anims";
|
||||||
|
import { allMoves } from "#data/data-lists";
|
||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import { HitResult } from "#enums/hit-result";
|
||||||
|
import { CommonAnim } from "#enums/move-anims-common";
|
||||||
|
import { MoveCategory } from "#enums/move-category";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { MoveTarget } from "#enums/move-target";
|
||||||
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import { Stat } from "#enums/stat";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
|
import type { Arena } from "#field/arena";
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
import type {
|
||||||
|
ArenaScreenTagType,
|
||||||
|
ArenaTagData,
|
||||||
|
EntryHazardTagType,
|
||||||
|
RoomArenaTagType,
|
||||||
|
SerializableArenaTagType,
|
||||||
|
} from "#types/arena-tags";
|
||||||
|
import type { Mutable } from "#types/type-helpers";
|
||||||
|
import { BooleanHolder, type NumberHolder, toDmgValue } from "#utils/common";
|
||||||
|
import i18next from "i18next";
|
||||||
|
|
||||||
/** Interface containing the serializable fields of ArenaTagData. */
|
/** Interface containing the serializable fields of ArenaTagData. */
|
||||||
interface BaseArenaTag {
|
interface BaseArenaTag {
|
||||||
/**
|
/**
|
||||||
@ -138,7 +138,7 @@ export abstract class ArenaTag implements BaseArenaTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onOverlap(_arena: Arena, _source: Pokemon | null): void {}
|
onOverlap(_arena: Arena, _source: Pokemon | undefined): void {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger this {@linkcode ArenaTag}'s effect, reducing its duration as applicable.
|
* Trigger this {@linkcode ArenaTag}'s effect, reducing its duration as applicable.
|
||||||
@ -172,9 +172,8 @@ export abstract class ArenaTag implements BaseArenaTag {
|
|||||||
/**
|
/**
|
||||||
* Helper function that retrieves the source Pokemon
|
* Helper function that retrieves the source Pokemon
|
||||||
* @returns - The source {@linkcode Pokemon} for this tag.
|
* @returns - The source {@linkcode Pokemon} for this tag.
|
||||||
* Returns `null` if `this.sourceId` is `undefined`
|
|
||||||
*/
|
*/
|
||||||
public getSourcePokemon(): Pokemon | null {
|
public getSourcePokemon(): Pokemon | undefined {
|
||||||
return globalScene.getPokemonById(this.sourceId);
|
return globalScene.getPokemonById(this.sourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -617,7 +616,7 @@ export class NoCritTag extends SerializableArenaTag {
|
|||||||
|
|
||||||
globalScene.phaseManager.queueMessage(
|
globalScene.phaseManager.queueMessage(
|
||||||
i18next.t("arenaTag:noCritOnRemove", {
|
i18next.t("arenaTag:noCritOnRemove", {
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(source ?? undefined),
|
pokemonNameWithAffix: getPokemonNameWithAffix(source),
|
||||||
moveName: this.getMoveName(),
|
moveName: this.getMoveName(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -1537,7 +1536,7 @@ export class SuppressAbilitiesTag extends SerializableArenaTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override onOverlap(_arena: Arena, source: Pokemon | null): void {
|
public override onOverlap(_arena: Arena, source: Pokemon | undefined): void {
|
||||||
(this as Mutable<this>).sourceCount++;
|
(this as Mutable<this>).sourceCount++;
|
||||||
this.playActivationMessage(source);
|
this.playActivationMessage(source);
|
||||||
}
|
}
|
||||||
@ -1580,7 +1579,7 @@ export class SuppressAbilitiesTag extends SerializableArenaTag {
|
|||||||
return this.sourceCount > 1;
|
return this.sourceCount > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private playActivationMessage(pokemon: Pokemon | null) {
|
private playActivationMessage(pokemon: Pokemon | undefined) {
|
||||||
if (pokemon) {
|
if (pokemon) {
|
||||||
globalScene.phaseManager.queueMessage(
|
globalScene.phaseManager.queueMessage(
|
||||||
i18next.t("arenaTag:neutralizingGasOnAdd", {
|
i18next.t("arenaTag:neutralizingGasOnAdd", {
|
||||||
|
235
src/data/balance/moveset-generation.ts
Normal file
235
src/data/balance/moveset-generation.ts
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-Copyright-Text: 2025 Pagefault Games
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* # Balance: Moveset Generation Configuration
|
||||||
|
*
|
||||||
|
* This module contains configuration constants and functions that control
|
||||||
|
* the limitations and rules around moveset generation for generated Pokémon.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* ### Move Weights
|
||||||
|
*
|
||||||
|
* The various move weight constants in this module control how likely
|
||||||
|
* certain categories of moves are to appear in a generated Pokémon's
|
||||||
|
* moveset. Higher weights make a move more likely to be chosen.
|
||||||
|
* The constants here specify the *base* weight for a move when first computed.
|
||||||
|
* These weights are post-processed (and then scaled up such that weights have a larger impact,
|
||||||
|
* for instance, on boss Pokémon) before being used in the actual moveset generation.
|
||||||
|
*
|
||||||
|
* Post Processing of weights includes, but is not limited to:
|
||||||
|
* - Adjusting weights of status moves
|
||||||
|
* - Adjusting weights based on the move's power relative to the highest power available
|
||||||
|
* - Adjusting weights based on the stat the move uses to calculate damage relative to the higher stat
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* All weights go through additional post-processing based on
|
||||||
|
* their expected power (accuracy * damage * expected number of hits)
|
||||||
|
*
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
|
||||||
|
|
||||||
|
//#region Constants
|
||||||
|
/**
|
||||||
|
* The minimum level for a Pokémon to generate with a move it can only learn
|
||||||
|
* from a common tier TM
|
||||||
|
*/
|
||||||
|
export const COMMON_TIER_TM_LEVEL_REQUIREMENT = 25;
|
||||||
|
/**
|
||||||
|
* The minimum level for a Pokémon to generate with a move it can only learn
|
||||||
|
* from a great tier TM
|
||||||
|
*/
|
||||||
|
export const GREAT_TIER_TM_LEVEL_REQUIREMENT = 40;
|
||||||
|
/**
|
||||||
|
* The minimum level for a Pokémon to generate with a move it can only learn
|
||||||
|
* from an ultra tier TM
|
||||||
|
*/
|
||||||
|
export const ULTRA_TIER_TM_LEVEL_REQUIREMENT = 55;
|
||||||
|
|
||||||
|
/** Below this level, Pokémon will be unable to generate with any egg moves */
|
||||||
|
export const EGG_MOVE_LEVEL_REQUIREMENT = 60;
|
||||||
|
/** Below this level, Pokémon will be unable to generate with rare egg moves */
|
||||||
|
export const RARE_EGG_MOVE_LEVEL_REQUIREMENT = 170;
|
||||||
|
|
||||||
|
// Note: Not exported, only for use with `getMaxTmCount
|
||||||
|
/** Below this level, Pokémon will be unable to generate with any TMs */
|
||||||
|
const ONE_TM_THRESHOLD = 25;
|
||||||
|
/** Below this level, Pokémon will generate with at most 1 TM */
|
||||||
|
const TWO_TM_THRESHOLD = 41;
|
||||||
|
/** Below this level, Pokémon will generate with at most two TMs */
|
||||||
|
const THREE_TM_THRESHOLD = 71;
|
||||||
|
/** Below this level, Pokémon will generate with at most three TMs */
|
||||||
|
const FOUR_TM_THRESHOLD = 101;
|
||||||
|
|
||||||
|
/** Below this level, Pokémon will be unable to generate any egg moves */
|
||||||
|
const ONE_EGG_MOVE_THRESHOLD = 80;
|
||||||
|
/** Below this level, Pokémon will generate with at most 1 egg moves */
|
||||||
|
const TWO_EGG_MOVE_THRESHOLD = 121;
|
||||||
|
/** Below this level, Pokémon will generate with at most 2 egg moves */
|
||||||
|
const THREE_EGG_MOVE_THRESHOLD = 161;
|
||||||
|
/** Above this level, Pokémon will generate with at most 3 egg moves */
|
||||||
|
const FOUR_EGG_MOVE_THRESHOLD = 201;
|
||||||
|
|
||||||
|
|
||||||
|
/** The weight given to TMs in the common tier during moveset generation */
|
||||||
|
export const COMMON_TM_MOVESET_WEIGHT = 12;
|
||||||
|
/** The weight given to TMs in the great tier during moveset generation */
|
||||||
|
export const GREAT_TM_MOVESET_WEIGHT = 14;
|
||||||
|
/** The weight given to TMs in the ultra tier during moveset generation */
|
||||||
|
export const ULTRA_TM_MOVESET_WEIGHT = 18;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base weight offset for level moves
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* The relative likelihood of moves learned at different levels is determined by
|
||||||
|
* the ratio of their weights,
|
||||||
|
* or, the formula:
|
||||||
|
* `(levelB + BASE_LEVEL_WEIGHT_OFFSET) / (levelA + BASE_LEVEL_WEIGHT_OFFSET)`
|
||||||
|
*
|
||||||
|
* For example, consider move A and B that are learned at levels 1 and 60, respectively,
|
||||||
|
* but have no other differences (same power, accuracy, category, etc).
|
||||||
|
* The following table demonstrates the likelihood of move B being chosen over move A.
|
||||||
|
*
|
||||||
|
* | Offset | Likelihood |
|
||||||
|
* |--------|------------|
|
||||||
|
* | 0 | 60x |
|
||||||
|
* | 1 | 30x |
|
||||||
|
* | 5 | 10.8x |
|
||||||
|
* | 20 | 3.8x |
|
||||||
|
* | 60 | 2x |
|
||||||
|
*
|
||||||
|
* Note that increasing this without adjusting the other weights will decrease the likelihood of non-level moves
|
||||||
|
*
|
||||||
|
* For a complete picture, see {@link https://www.desmos.com/calculator/wgln4dxigl}
|
||||||
|
*/
|
||||||
|
export const BASE_LEVEL_WEIGHT_OFFSET = 20;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum weight an egg move can ever have
|
||||||
|
* @remarks
|
||||||
|
* Egg moves have their weights adjusted based on the maximum weight of the Pokémon's
|
||||||
|
* level-up moves. Rare Egg moves are always 5/6th of the computed egg move weight.
|
||||||
|
* Boss pokemon are not allowed to spawn with rare egg moves.
|
||||||
|
* @see {@linkcode EGG_MOVE_TO_LEVEL_WEIGHT}
|
||||||
|
*/
|
||||||
|
export const EGG_MOVE_WEIGHT_MAX = 60;
|
||||||
|
/**
|
||||||
|
* The percentage of the Pokémon's highest weighted level move to the weight an
|
||||||
|
* egg move can generate with
|
||||||
|
*/
|
||||||
|
export const EGG_MOVE_TO_LEVEL_WEIGHT = 0.85;
|
||||||
|
/** The weight given to evolution moves */
|
||||||
|
export const EVOLUTION_MOVE_WEIGHT = 70;
|
||||||
|
/** The weight given to relearn moves */
|
||||||
|
export const RELEARN_MOVE_WEIGHT = 60;
|
||||||
|
|
||||||
|
/** The base weight multiplier to use
|
||||||
|
*
|
||||||
|
* The higher the number, the more impact weights have on the final move selection.
|
||||||
|
* i.e. if set to 0, all moves have equal chance of being selected regardless of their weight.
|
||||||
|
*/
|
||||||
|
export const BASE_WEIGHT_MULTIPLIER = 1.6;
|
||||||
|
|
||||||
|
/** The additional weight added onto {@linkcode BASE_WEIGHT_MULTIPLIER} for boss Pokémon */
|
||||||
|
export const BOSS_EXTRA_WEIGHT_MULTIPLIER = 0.4;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of moves that should be blacklisted from the forced STAB during moveset generation
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* During moveset generation, trainer pokemon attempt to force their pokemon to generate with STAB
|
||||||
|
* moves in their movesets. Moves in this list not be considered to be "STAB" moves for this purpose.
|
||||||
|
* This does *not* prevent them from appearing in the moveset, but they will never
|
||||||
|
* be selected as a forced STAB move.
|
||||||
|
*/
|
||||||
|
export const STAB_BLACKLIST: ReadonlySet<MoveId> = new Set([
|
||||||
|
MoveId.BEAT_UP,
|
||||||
|
MoveId.BELCH,
|
||||||
|
MoveId.BIDE,
|
||||||
|
MoveId.COMEUPPANCE,
|
||||||
|
MoveId.COUNTER,
|
||||||
|
MoveId.DOOM_DESIRE,
|
||||||
|
MoveId.DRAGON_RAGE,
|
||||||
|
MoveId.DREAM_EATER,
|
||||||
|
MoveId.ENDEAVOR,
|
||||||
|
MoveId.EXPLOSION,
|
||||||
|
MoveId.FAKE_OUT,
|
||||||
|
MoveId.FIRST_IMPRESSION,
|
||||||
|
MoveId.FISSURE,
|
||||||
|
MoveId.FLING,
|
||||||
|
MoveId.FOCUS_PUNCH,
|
||||||
|
MoveId.FUTURE_SIGHT,
|
||||||
|
MoveId.GUILLOTINE,
|
||||||
|
MoveId.HOLD_BACK,
|
||||||
|
MoveId.HORN_DRILL,
|
||||||
|
MoveId.LAST_RESORT,
|
||||||
|
MoveId.METAL_BURST,
|
||||||
|
MoveId.MIRROR_COAT,
|
||||||
|
MoveId.MISTY_EXPLOSION,
|
||||||
|
MoveId.NATURAL_GIFT,
|
||||||
|
MoveId.NATURES_MADNESS,
|
||||||
|
MoveId.NIGHT_SHADE,
|
||||||
|
MoveId.PSYWAVE,
|
||||||
|
MoveId.RUINATION,
|
||||||
|
MoveId.SELF_DESTRUCT,
|
||||||
|
MoveId.SHEER_COLD,
|
||||||
|
MoveId.SHELL_TRAP,
|
||||||
|
MoveId.SKY_DROP,
|
||||||
|
MoveId.SNORE,
|
||||||
|
MoveId.SONIC_BOOM,
|
||||||
|
MoveId.SPIT_UP,
|
||||||
|
MoveId.STEEL_BEAM,
|
||||||
|
MoveId.STEEL_ROLLER,
|
||||||
|
MoveId.SUPER_FANG,
|
||||||
|
MoveId.SYNCHRONOISE,
|
||||||
|
MoveId.UPPER_HAND,
|
||||||
|
]);
|
||||||
|
|
||||||
|
//#endregion Constants
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum number of TMs a Pokémon is allowed to learn based on
|
||||||
|
* its level
|
||||||
|
* @param level - The level of the Pokémon
|
||||||
|
* @returns The number of TMs the Pokémon can learn at this level
|
||||||
|
*/
|
||||||
|
export function getMaxTmCount(level: number) {
|
||||||
|
if (level < ONE_TM_THRESHOLD) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (level < TWO_TM_THRESHOLD) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (level < THREE_TM_THRESHOLD) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (level < FOUR_TM_THRESHOLD) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function getMaxEggMoveCount(level: number): number {
|
||||||
|
if (level < ONE_EGG_MOVE_THRESHOLD) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (level < TWO_EGG_MOVE_THRESHOLD) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (level < THREE_EGG_MOVE_THRESHOLD) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (level < FOUR_EGG_MOVE_THRESHOLD) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
return 4;
|
||||||
|
}
|
@ -14,7 +14,7 @@ import { TimeOfDay } from "#enums/time-of-day";
|
|||||||
import { WeatherType } from "#enums/weather-type";
|
import { WeatherType } from "#enums/weather-type";
|
||||||
import type { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import type { SpeciesStatBoosterItem, SpeciesStatBoosterModifierType } from "#modifiers/modifier-type";
|
import type { SpeciesStatBoosterItem, SpeciesStatBoosterModifierType } from "#modifiers/modifier-type";
|
||||||
import { coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common";
|
import { coerceArray, randSeedInt } from "#utils/common";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
import { toCamelCase } from "#utils/strings";
|
import { toCamelCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
@ -53,6 +53,7 @@ export enum EvolutionItem {
|
|||||||
PRISM_SCALE,
|
PRISM_SCALE,
|
||||||
RAZOR_CLAW,
|
RAZOR_CLAW,
|
||||||
RAZOR_FANG,
|
RAZOR_FANG,
|
||||||
|
OVAL_STONE,
|
||||||
REAPER_CLOTH,
|
REAPER_CLOTH,
|
||||||
ELECTIRIZER,
|
ELECTIRIZER,
|
||||||
MAGMARIZER,
|
MAGMARIZER,
|
||||||
@ -128,7 +129,7 @@ export class SpeciesEvolutionCondition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get description(): string[] {
|
public get description(): string[] {
|
||||||
if (!isNullOrUndefined(this.desc)) {
|
if (this.desc != null) {
|
||||||
return this.desc;
|
return this.desc;
|
||||||
}
|
}
|
||||||
this.desc = this.data.map(cond => {
|
this.desc = this.data.map(cond => {
|
||||||
@ -161,11 +162,11 @@ export class SpeciesEvolutionCondition {
|
|||||||
case EvoCondKey.HELD_ITEM:
|
case EvoCondKey.HELD_ITEM:
|
||||||
return i18next.t(`pokemonEvolutions:heldItem.${toCamelCase(cond.itemKey)}`);
|
return i18next.t(`pokemonEvolutions:heldItem.${toCamelCase(cond.itemKey)}`);
|
||||||
}
|
}
|
||||||
}).filter(s => !isNullOrUndefined(s)); // Filter out stringless conditions
|
}).filter(s => s != null); // Filter out stringless conditions
|
||||||
return this.desc;
|
return this.desc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public conditionsFulfilled(pokemon: Pokemon): boolean {
|
public conditionsFulfilled(pokemon: Pokemon, forFusion = false): boolean {
|
||||||
console.log(this.data);
|
console.log(this.data);
|
||||||
return this.data.every(cond => {
|
return this.data.every(cond => {
|
||||||
switch (cond.key) {
|
switch (cond.key) {
|
||||||
@ -185,7 +186,7 @@ export class SpeciesEvolutionCondition {
|
|||||||
m.getStackCount() + pokemon.getPersistentTreasureCount() >= cond.value
|
m.getStackCount() + pokemon.getPersistentTreasureCount() >= cond.value
|
||||||
);
|
);
|
||||||
case EvoCondKey.GENDER:
|
case EvoCondKey.GENDER:
|
||||||
return pokemon.gender === cond.gender;
|
return cond.gender === (forFusion ? pokemon.fusionGender : pokemon.gender);
|
||||||
case EvoCondKey.SHEDINJA: // Shedinja cannot be evolved into directly
|
case EvoCondKey.SHEDINJA: // Shedinja cannot be evolved into directly
|
||||||
return false;
|
return false;
|
||||||
case EvoCondKey.BIOME:
|
case EvoCondKey.BIOME:
|
||||||
@ -233,7 +234,7 @@ export class SpeciesFormEvolution {
|
|||||||
this.evoFormKey = evoFormKey;
|
this.evoFormKey = evoFormKey;
|
||||||
this.level = level;
|
this.level = level;
|
||||||
this.item = item || EvolutionItem.NONE;
|
this.item = item || EvolutionItem.NONE;
|
||||||
if (!isNullOrUndefined(condition)) {
|
if (condition != null) {
|
||||||
this.condition = new SpeciesEvolutionCondition(...coerceArray(condition));
|
this.condition = new SpeciesEvolutionCondition(...coerceArray(condition));
|
||||||
}
|
}
|
||||||
this.wildDelay = wildDelay ?? SpeciesWildEvolutionDelay.NONE;
|
this.wildDelay = wildDelay ?? SpeciesWildEvolutionDelay.NONE;
|
||||||
@ -291,8 +292,8 @@ export class SpeciesFormEvolution {
|
|||||||
return (
|
return (
|
||||||
pokemon.level >= this.level &&
|
pokemon.level >= this.level &&
|
||||||
// Check form key, using the fusion's form key if we're checking the fusion
|
// Check form key, using the fusion's form key if we're checking the fusion
|
||||||
(isNullOrUndefined(this.preFormKey) || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) &&
|
(this.preFormKey == null || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) &&
|
||||||
(isNullOrUndefined(this.condition) || this.condition.conditionsFulfilled(pokemon)) &&
|
(this.condition == null || this.condition.conditionsFulfilled(pokemon, forFusion)) &&
|
||||||
((item ?? EvolutionItem.NONE) === (this.item ?? EvolutionItem.NONE))
|
((item ?? EvolutionItem.NONE) === (this.item ?? EvolutionItem.NONE))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -305,11 +306,11 @@ export class SpeciesFormEvolution {
|
|||||||
*/
|
*/
|
||||||
public isValidItemEvolution(pokemon: Pokemon, forFusion = false): boolean {
|
public isValidItemEvolution(pokemon: Pokemon, forFusion = false): boolean {
|
||||||
return (
|
return (
|
||||||
!isNullOrUndefined(this.item) &&
|
this.item != null &&
|
||||||
pokemon.level >= this.level &&
|
pokemon.level >= this.level &&
|
||||||
// Check form key, using the fusion's form key if we're checking the fusion
|
// Check form key, using the fusion's form key if we're checking the fusion
|
||||||
(isNullOrUndefined(this.preFormKey) || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) &&
|
(this.preFormKey == null || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) &&
|
||||||
(isNullOrUndefined(this.condition) || this.condition.conditionsFulfilled(pokemon))
|
(this.condition == null || this.condition.conditionsFulfilled(pokemon))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1496,10 +1497,13 @@ export const pokemonEvolutions: PokemonEvolutions = {
|
|||||||
new SpeciesFormEvolution(SpeciesId.DUDUNSPARCE, "", "two-segment", 32, null, {key: EvoCondKey.MOVE, move: MoveId.HYPER_DRILL}, SpeciesWildEvolutionDelay.LONG)
|
new SpeciesFormEvolution(SpeciesId.DUDUNSPARCE, "", "two-segment", 32, null, {key: EvoCondKey.MOVE, move: MoveId.HYPER_DRILL}, SpeciesWildEvolutionDelay.LONG)
|
||||||
],
|
],
|
||||||
[SpeciesId.GLIGAR]: [
|
[SpeciesId.GLIGAR]: [
|
||||||
new SpeciesEvolution(SpeciesId.GLISCOR, 1, EvolutionItem.RAZOR_FANG, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]} /* Razor fang at night*/, SpeciesWildEvolutionDelay.VERY_LONG)
|
new SpeciesEvolution(SpeciesId.GLISCOR, 1, EvolutionItem.RAZOR_FANG, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}, SpeciesWildEvolutionDelay.VERY_LONG)
|
||||||
],
|
],
|
||||||
[SpeciesId.SNEASEL]: [
|
[SpeciesId.SNEASEL]: [
|
||||||
new SpeciesEvolution(SpeciesId.WEAVILE, 1, EvolutionItem.RAZOR_CLAW, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]} /* Razor claw at night*/, SpeciesWildEvolutionDelay.VERY_LONG)
|
new SpeciesEvolution(SpeciesId.WEAVILE, 1, EvolutionItem.RAZOR_CLAW, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}, SpeciesWildEvolutionDelay.VERY_LONG)
|
||||||
|
],
|
||||||
|
[SpeciesId.HAPPINY]: [
|
||||||
|
new SpeciesEvolution(SpeciesId.CHANSEY, 1, EvolutionItem.OVAL_STONE, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}, SpeciesWildEvolutionDelay.SHORT)
|
||||||
],
|
],
|
||||||
[SpeciesId.URSARING]: [
|
[SpeciesId.URSARING]: [
|
||||||
new SpeciesEvolution(SpeciesId.URSALUNA, 1, EvolutionItem.PEAT_BLOCK, null, SpeciesWildEvolutionDelay.VERY_LONG) //Ursaring does not evolve into Bloodmoon Ursaluna
|
new SpeciesEvolution(SpeciesId.URSALUNA, 1, EvolutionItem.PEAT_BLOCK, null, SpeciesWildEvolutionDelay.VERY_LONG) //Ursaring does not evolve into Bloodmoon Ursaluna
|
||||||
@ -1760,7 +1764,7 @@ export const pokemonEvolutions: PokemonEvolutions = {
|
|||||||
new SpeciesEvolution(SpeciesId.CROBAT, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 120}, SpeciesWildEvolutionDelay.VERY_LONG)
|
new SpeciesEvolution(SpeciesId.CROBAT, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 120}, SpeciesWildEvolutionDelay.VERY_LONG)
|
||||||
],
|
],
|
||||||
[SpeciesId.CHANSEY]: [
|
[SpeciesId.CHANSEY]: [
|
||||||
new SpeciesEvolution(SpeciesId.BLISSEY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 200}, SpeciesWildEvolutionDelay.LONG)
|
new SpeciesEvolution(SpeciesId.BLISSEY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 180}, SpeciesWildEvolutionDelay.LONG)
|
||||||
],
|
],
|
||||||
[SpeciesId.PICHU]: [
|
[SpeciesId.PICHU]: [
|
||||||
new SpeciesFormEvolution(SpeciesId.PIKACHU, "spiky", "partner", 1, null, {key: EvoCondKey.FRIENDSHIP, value: 90}, SpeciesWildEvolutionDelay.SHORT),
|
new SpeciesFormEvolution(SpeciesId.PIKACHU, "spiky", "partner", 1, null, {key: EvoCondKey.FRIENDSHIP, value: 90}, SpeciesWildEvolutionDelay.SHORT),
|
||||||
@ -1787,9 +1791,6 @@ export const pokemonEvolutions: PokemonEvolutions = {
|
|||||||
[SpeciesId.CHINGLING]: [
|
[SpeciesId.CHINGLING]: [
|
||||||
new SpeciesEvolution(SpeciesId.CHIMECHO, 1, null, [{key: EvoCondKey.FRIENDSHIP, value: 90}, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}], SpeciesWildEvolutionDelay.MEDIUM)
|
new SpeciesEvolution(SpeciesId.CHIMECHO, 1, null, [{key: EvoCondKey.FRIENDSHIP, value: 90}, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}], SpeciesWildEvolutionDelay.MEDIUM)
|
||||||
],
|
],
|
||||||
[SpeciesId.HAPPINY]: [
|
|
||||||
new SpeciesEvolution(SpeciesId.CHANSEY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 160}, SpeciesWildEvolutionDelay.SHORT)
|
|
||||||
],
|
|
||||||
[SpeciesId.MUNCHLAX]: [
|
[SpeciesId.MUNCHLAX]: [
|
||||||
new SpeciesEvolution(SpeciesId.SNORLAX, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 120}, SpeciesWildEvolutionDelay.LONG)
|
new SpeciesEvolution(SpeciesId.SNORLAX, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 120}, SpeciesWildEvolutionDelay.LONG)
|
||||||
],
|
],
|
||||||
|
@ -7,7 +7,7 @@ import { AnimBlendType, AnimFocus, AnimFrameTarget, ChargeAnim, CommonAnim } fro
|
|||||||
import { MoveFlags } from "#enums/move-flags";
|
import { MoveFlags } from "#enums/move-flags";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import type { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import { coerceArray, getFrameMs, isNullOrUndefined, type nil } from "#utils/common";
|
import { coerceArray, getFrameMs, type nil } from "#utils/common";
|
||||||
import { getEnumKeys, getEnumValues } from "#utils/enums";
|
import { getEnumKeys, getEnumValues } from "#utils/enums";
|
||||||
import { toKebabCase } from "#utils/strings";
|
import { toKebabCase } from "#utils/strings";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
@ -388,7 +388,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent {
|
|||||||
moveAnim.bgSprite.setAlpha(this.opacity / 255);
|
moveAnim.bgSprite.setAlpha(this.opacity / 255);
|
||||||
globalScene.field.add(moveAnim.bgSprite);
|
globalScene.field.add(moveAnim.bgSprite);
|
||||||
const fieldPokemon = globalScene.getEnemyPokemon(false) ?? globalScene.getPlayerPokemon(false);
|
const fieldPokemon = globalScene.getEnemyPokemon(false) ?? globalScene.getPlayerPokemon(false);
|
||||||
if (!isNullOrUndefined(priority)) {
|
if (priority != null) {
|
||||||
globalScene.field.moveTo(moveAnim.bgSprite as Phaser.GameObjects.GameObject, priority);
|
globalScene.field.moveTo(moveAnim.bgSprite as Phaser.GameObjects.GameObject, priority);
|
||||||
} else if (fieldPokemon?.isOnField()) {
|
} else if (fieldPokemon?.isOnField()) {
|
||||||
globalScene.field.moveBelow(moveAnim.bgSprite as Phaser.GameObjects.GameObject, fieldPokemon);
|
globalScene.field.moveBelow(moveAnim.bgSprite as Phaser.GameObjects.GameObject, fieldPokemon);
|
||||||
@ -524,7 +524,7 @@ export async function initEncounterAnims(encounterAnim: EncounterAnim | Encounte
|
|||||||
const encounterAnimNames = getEnumKeys(EncounterAnim);
|
const encounterAnimNames = getEnumKeys(EncounterAnim);
|
||||||
const encounterAnimFetches: Promise<Map<EncounterAnim, AnimConfig>>[] = [];
|
const encounterAnimFetches: Promise<Map<EncounterAnim, AnimConfig>>[] = [];
|
||||||
for (const anim of anims) {
|
for (const anim of anims) {
|
||||||
if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) {
|
if (encounterAnims.has(anim) && encounterAnims.get(anim) != null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
encounterAnimFetches.push(
|
encounterAnimFetches.push(
|
||||||
@ -1240,7 +1240,7 @@ export abstract class BattleAnim {
|
|||||||
|
|
||||||
const graphicIndex = graphicFrameCount++;
|
const graphicIndex = graphicFrameCount++;
|
||||||
const moveSprite = sprites[graphicIndex];
|
const moveSprite = sprites[graphicIndex];
|
||||||
if (!isNullOrUndefined(frame.priority)) {
|
if (frame.priority != null) {
|
||||||
const setSpritePriority = (priority: number) => {
|
const setSpritePriority = (priority: number) => {
|
||||||
if (existingFieldSprites.length > priority) {
|
if (existingFieldSprites.length > priority) {
|
||||||
// Move to specified priority index
|
// Move to specified priority index
|
||||||
|
@ -1,3 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* BattlerTags are used to represent semi-persistent effects that can be attached to a Pokemon.
|
||||||
|
* Note that before serialization, a new tag object is created, and then `loadTag` is called on the
|
||||||
|
* tag with the object that was serialized.
|
||||||
|
*
|
||||||
|
* This means it is straightforward to avoid serializing fields.
|
||||||
|
* Fields that are not set in the constructor and not set in `loadTag` will thus not be serialized.
|
||||||
|
*
|
||||||
|
* Any battler tag that can persist across sessions must extend SerializableBattlerTag in its class definition signature.
|
||||||
|
* Only tags that persist across waves (meaning their effect can last >1 turn) should be considered
|
||||||
|
* serializable.
|
||||||
|
*
|
||||||
|
* Serializable battler tags have strict requirements for their fields.
|
||||||
|
* Properties that are not necessary to reconstruct the tag must not be serialized. This can be avoided
|
||||||
|
* by using a private property. If access to the property is needed outside of the class, then
|
||||||
|
* a getter (and potentially, a setter) should be used instead.
|
||||||
|
*
|
||||||
|
* If a property that is intended to be private must be serialized, then it should instead
|
||||||
|
* be declared as a public readonly propety. Then, in the `loadTag` method (or any method inside the class that needs to adjust the property)
|
||||||
|
* use `(this as Mutable<this>).propertyName = value;`
|
||||||
|
* These rules ensure that Typescript is aware of the shape of the serialized version of the class.
|
||||||
|
*
|
||||||
|
* If any new serializable fields *are* added, then the class *must* override the
|
||||||
|
* `loadTag` method to set the new fields. Its signature *must* match the example below:
|
||||||
|
* ```
|
||||||
|
* class ExampleTag extends SerializableBattlerTag {
|
||||||
|
* // Example, if we add 2 new fields that should be serialized:
|
||||||
|
* public a: string;
|
||||||
|
* public b: number;
|
||||||
|
* // Then we must also define a loadTag method with one of the following signatures
|
||||||
|
* public override loadTag(source: BaseBattlerTag & Pick<ExampleTag, "tagType" | "a" | "b"): void;
|
||||||
|
* public override loadTag<const T extends this>(source: BaseBattlerTag & Pick<T, "tagType" | "a" | "b">): void;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* Notes
|
||||||
|
* - If the class has any subclasses, then the second form of `loadTag` *must* be used.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
@ -51,48 +90,9 @@ import type {
|
|||||||
TypeBoostTagType,
|
TypeBoostTagType,
|
||||||
} from "#types/battler-tags";
|
} from "#types/battler-tags";
|
||||||
import type { Mutable } from "#types/type-helpers";
|
import type { Mutable } from "#types/type-helpers";
|
||||||
import { BooleanHolder, coerceArray, getFrameMs, isNullOrUndefined, NumberHolder, toDmgValue } from "#utils/common";
|
import { BooleanHolder, coerceArray, getFrameMs, NumberHolder, toDmgValue } from "#utils/common";
|
||||||
import { toCamelCase } from "#utils/strings";
|
import { toCamelCase } from "#utils/strings";
|
||||||
|
|
||||||
/**
|
|
||||||
* @module
|
|
||||||
* BattlerTags are used to represent semi-persistent effects that can be attached to a Pokemon.
|
|
||||||
* Note that before serialization, a new tag object is created, and then `loadTag` is called on the
|
|
||||||
* tag with the object that was serialized.
|
|
||||||
*
|
|
||||||
* This means it is straightforward to avoid serializing fields.
|
|
||||||
* Fields that are not set in the constructor and not set in `loadTag` will thus not be serialized.
|
|
||||||
*
|
|
||||||
* Any battler tag that can persist across sessions must extend SerializableBattlerTag in its class definition signature.
|
|
||||||
* Only tags that persist across waves (meaning their effect can last >1 turn) should be considered
|
|
||||||
* serializable.
|
|
||||||
*
|
|
||||||
* Serializable battler tags have strict requirements for their fields.
|
|
||||||
* Properties that are not necessary to reconstruct the tag must not be serialized. This can be avoided
|
|
||||||
* by using a private property. If access to the property is needed outside of the class, then
|
|
||||||
* a getter (and potentially, a setter) should be used instead.
|
|
||||||
*
|
|
||||||
* If a property that is intended to be private must be serialized, then it should instead
|
|
||||||
* be declared as a public readonly propety. Then, in the `loadTag` method (or any method inside the class that needs to adjust the property)
|
|
||||||
* use `(this as Mutable<this>).propertyName = value;`
|
|
||||||
* These rules ensure that Typescript is aware of the shape of the serialized version of the class.
|
|
||||||
*
|
|
||||||
* If any new serializable fields *are* added, then the class *must* override the
|
|
||||||
* `loadTag` method to set the new fields. Its signature *must* match the example below:
|
|
||||||
* ```
|
|
||||||
* class ExampleTag extends SerializableBattlerTag {
|
|
||||||
* // Example, if we add 2 new fields that should be serialized:
|
|
||||||
* public a: string;
|
|
||||||
* public b: number;
|
|
||||||
* // Then we must also define a loadTag method with one of the following signatures
|
|
||||||
* public override loadTag(source: BaseBattlerTag & Pick<ExampleTag, "tagType" | "a" | "b"): void;
|
|
||||||
* public override loadTag<const T extends this>(source: BaseBattlerTag & Pick<T, "tagType" | "a" | "b">): void;
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
* Notes
|
|
||||||
* - If the class has any subclasses, then the second form of `loadTag` *must* be used.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Interface containing the serializable fields of BattlerTag */
|
/** Interface containing the serializable fields of BattlerTag */
|
||||||
interface BaseBattlerTag {
|
interface BaseBattlerTag {
|
||||||
/** The tag's remaining duration */
|
/** The tag's remaining duration */
|
||||||
@ -201,7 +201,7 @@ export class BattlerTag implements BaseBattlerTag {
|
|||||||
* Helper function that retrieves the source Pokemon object
|
* Helper function that retrieves the source Pokemon object
|
||||||
* @returns The source {@linkcode Pokemon}, or `null` if none is found
|
* @returns The source {@linkcode Pokemon}, or `null` if none is found
|
||||||
*/
|
*/
|
||||||
public getSourcePokemon(): Pokemon | null {
|
public getSourcePokemon(): Pokemon | undefined {
|
||||||
return globalScene.getPokemonById(this.sourceId);
|
return globalScene.getPokemonById(this.sourceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -381,7 +381,7 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
|
|||||||
// Disable fails against struggle or an empty move history
|
// Disable fails against struggle or an empty move history
|
||||||
// TODO: Confirm if this is redundant given Disable/Cursed Body's disable conditions
|
// TODO: Confirm if this is redundant given Disable/Cursed Body's disable conditions
|
||||||
const move = pokemon.getLastNonVirtualMove();
|
const move = pokemon.getLastNonVirtualMove();
|
||||||
if (isNullOrUndefined(move) || move.move === MoveId.STRUGGLE) {
|
if (move == null || move.move === MoveId.STRUGGLE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -454,7 +454,7 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
|
|||||||
override canAdd(pokemon: Pokemon): boolean {
|
override canAdd(pokemon: Pokemon): boolean {
|
||||||
// Choice items ignore struggle, so Gorilla Tactics should too
|
// Choice items ignore struggle, so Gorilla Tactics should too
|
||||||
const lastSelectedMove = pokemon.getLastNonVirtualMove();
|
const lastSelectedMove = pokemon.getLastNonVirtualMove();
|
||||||
return !isNullOrUndefined(lastSelectedMove) && lastSelectedMove.move !== MoveId.STRUGGLE;
|
return lastSelectedMove != null && lastSelectedMove.move !== MoveId.STRUGGLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -971,7 +971,7 @@ export class InfatuatedTag extends SerializableBattlerTag {
|
|||||||
phaseManager.queueMessage(
|
phaseManager.queueMessage(
|
||||||
i18next.t("battlerTags:infatuatedLapse", {
|
i18next.t("battlerTags:infatuatedLapse", {
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||||
sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
|
sourcePokemonName: getPokemonNameWithAffix(this.getSourcePokemon()),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, CommonAnim.ATTRACT);
|
phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, CommonAnim.ATTRACT);
|
||||||
@ -1327,20 +1327,18 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If the encored move has run out of PP or the tag's turn count has elapsed,
|
|
||||||
* Encore ends at the END of the turn.
|
|
||||||
* Otherwise, Encore's duration reduces when the target attempts to use a move.
|
|
||||||
* @returns Whether the tag should remain active.
|
|
||||||
*/
|
|
||||||
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||||
|
// If the encored move has run out of PP or the tag's turn count has elapsed,
|
||||||
|
// Encore ends at the END of the turn.
|
||||||
|
// Otherwise, Encore's duration reduces when the target attempts to use a move.
|
||||||
|
|
||||||
if (lapseType === BattlerTagLapseType.AFTER_MOVE) {
|
if (lapseType === BattlerTagLapseType.AFTER_MOVE) {
|
||||||
this.turnCount--;
|
this.turnCount--;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const encoredMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
const encoredMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
||||||
if (isNullOrUndefined(encoredMove) || encoredMove.isOutOfPp()) {
|
if (encoredMove == null || encoredMove.isOutOfPp()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.turnCount > 0;
|
return this.turnCount > 0;
|
||||||
|
@ -7,7 +7,7 @@ import { BiomeId } from "#enums/biome-id";
|
|||||||
import { PartyMemberStrength } from "#enums/party-member-strength";
|
import { PartyMemberStrength } from "#enums/party-member-strength";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import type { Starter } from "#ui/starter-select-ui-handler";
|
import type { Starter } from "#ui/starter-select-ui-handler";
|
||||||
import { isNullOrUndefined, randSeedGauss, randSeedInt, randSeedItem } from "#utils/common";
|
import { randSeedGauss, randSeedInt, randSeedItem } from "#utils/common";
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ export function getDailyRunStarters(seed: string): Starter[] {
|
|||||||
const startingLevel = globalScene.gameMode.getStartingLevel();
|
const startingLevel = globalScene.gameMode.getStartingLevel();
|
||||||
|
|
||||||
const eventStarters = getDailyEventSeedStarters(seed);
|
const eventStarters = getDailyEventSeedStarters(seed);
|
||||||
if (!isNullOrUndefined(eventStarters)) {
|
if (eventStarters != null) {
|
||||||
starters.push(...eventStarters);
|
starters.push(...eventStarters);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -127,7 +127,7 @@ const dailyBiomeWeights: BiomeWeights = {
|
|||||||
|
|
||||||
export function getDailyStartingBiome(): BiomeId {
|
export function getDailyStartingBiome(): BiomeId {
|
||||||
const eventBiome = getDailyEventSeedBiome(globalScene.seed);
|
const eventBiome = getDailyEventSeedBiome(globalScene.seed);
|
||||||
if (!isNullOrUndefined(eventBiome)) {
|
if (eventBiome != null) {
|
||||||
return eventBiome;
|
return eventBiome;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import { PokemonType } from "#enums/pokemon-type";
|
|||||||
import type { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import { applyMoveAttrs } from "#moves/apply-attrs";
|
import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||||
import type { Move, MoveTargetSet, UserMoveConditionFunc } from "#moves/move";
|
import type { Move, MoveTargetSet, UserMoveConditionFunc } from "#moves/move";
|
||||||
import { isNullOrUndefined, NumberHolder } from "#utils/common";
|
import { NumberHolder } from "#utils/common";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return whether the move targets the field
|
* Return whether the move targets the field
|
||||||
@ -78,7 +78,7 @@ export function getMoveTargets(user: Pokemon, move: MoveId, replaceTarget?: Move
|
|||||||
case MoveTarget.OTHER:
|
case MoveTarget.OTHER:
|
||||||
case MoveTarget.ALL_NEAR_OTHERS:
|
case MoveTarget.ALL_NEAR_OTHERS:
|
||||||
case MoveTarget.ALL_OTHERS:
|
case MoveTarget.ALL_OTHERS:
|
||||||
set = !isNullOrUndefined(ally) ? opponents.concat([ally]) : opponents;
|
set = ally != null ? opponents.concat([ally]) : opponents;
|
||||||
multiple = moveTarget === MoveTarget.ALL_NEAR_OTHERS || moveTarget === MoveTarget.ALL_OTHERS;
|
multiple = moveTarget === MoveTarget.ALL_NEAR_OTHERS || moveTarget === MoveTarget.ALL_OTHERS;
|
||||||
break;
|
break;
|
||||||
case MoveTarget.NEAR_ENEMY:
|
case MoveTarget.NEAR_ENEMY:
|
||||||
@ -95,22 +95,22 @@ export function getMoveTargets(user: Pokemon, move: MoveId, replaceTarget?: Move
|
|||||||
return { targets: [-1 as BattlerIndex], multiple: false };
|
return { targets: [-1 as BattlerIndex], multiple: false };
|
||||||
case MoveTarget.NEAR_ALLY:
|
case MoveTarget.NEAR_ALLY:
|
||||||
case MoveTarget.ALLY:
|
case MoveTarget.ALLY:
|
||||||
set = !isNullOrUndefined(ally) ? [ally] : [];
|
set = ally != null ? [ally] : [];
|
||||||
break;
|
break;
|
||||||
case MoveTarget.USER_OR_NEAR_ALLY:
|
case MoveTarget.USER_OR_NEAR_ALLY:
|
||||||
case MoveTarget.USER_AND_ALLIES:
|
case MoveTarget.USER_AND_ALLIES:
|
||||||
case MoveTarget.USER_SIDE:
|
case MoveTarget.USER_SIDE:
|
||||||
set = !isNullOrUndefined(ally) ? [user, ally] : [user];
|
set = ally != null ? [user, ally] : [user];
|
||||||
multiple = moveTarget !== MoveTarget.USER_OR_NEAR_ALLY;
|
multiple = moveTarget !== MoveTarget.USER_OR_NEAR_ALLY;
|
||||||
break;
|
break;
|
||||||
case MoveTarget.ALL:
|
case MoveTarget.ALL:
|
||||||
case MoveTarget.BOTH_SIDES:
|
case MoveTarget.BOTH_SIDES:
|
||||||
set = (!isNullOrUndefined(ally) ? [user, ally] : [user]).concat(opponents);
|
set = (ally != null ? [user, ally] : [user]).concat(opponents);
|
||||||
multiple = true;
|
multiple = true;
|
||||||
break;
|
break;
|
||||||
case MoveTarget.CURSE:
|
case MoveTarget.CURSE:
|
||||||
{
|
{
|
||||||
const extraTargets = !isNullOrUndefined(ally) ? [ally] : [];
|
const extraTargets = ally != null ? [ally] : [];
|
||||||
set = user.getTypes(true).includes(PokemonType.GHOST) ? opponents.concat(extraTargets) : [user];
|
set = user.getTypes(true).includes(PokemonType.GHOST) ? opponents.concat(extraTargets) : [user];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -89,7 +89,7 @@ import type { AttackMoveResult } from "#types/attack-move-result";
|
|||||||
import type { Localizable } from "#types/locales";
|
import type { Localizable } from "#types/locales";
|
||||||
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
|
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
|
||||||
import type { TurnMove } from "#types/turn-move";
|
import type { TurnMove } from "#types/turn-move";
|
||||||
import { BooleanHolder, coerceArray, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
import { BooleanHolder, coerceArray, type Constructor, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
import { toCamelCase, toTitleCase } from "#utils/strings";
|
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
@ -825,7 +825,7 @@ export abstract class Move implements Localizable {
|
|||||||
|
|
||||||
applyAbAttrs("VariableMovePowerAbAttr", abAttrParams);
|
applyAbAttrs("VariableMovePowerAbAttr", abAttrParams);
|
||||||
const ally = source.getAlly();
|
const ally = source.getAlly();
|
||||||
if (!isNullOrUndefined(ally)) {
|
if (ally != null) {
|
||||||
applyAbAttrs("AllyMoveCategoryPowerBoostAbAttr", {...abAttrParams, pokemon: ally});
|
applyAbAttrs("AllyMoveCategoryPowerBoostAbAttr", {...abAttrParams, pokemon: ally});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -955,7 +955,7 @@ export abstract class Move implements Localizable {
|
|||||||
|
|
||||||
// ...and cannot enhance Pollen Puff when targeting an ally.
|
// ...and cannot enhance Pollen Puff when targeting an ally.
|
||||||
const ally = user.getAlly();
|
const ally = user.getAlly();
|
||||||
const exceptPollenPuffAlly: boolean = this.id === MoveId.POLLEN_PUFF && !isNullOrUndefined(ally) && targets.includes(ally.getBattlerIndex())
|
const exceptPollenPuffAlly: boolean = this.id === MoveId.POLLEN_PUFF && ally != null && targets.includes(ally.getBattlerIndex())
|
||||||
|
|
||||||
return (!restrictSpread || !isMultiTarget)
|
return (!restrictSpread || !isMultiTarget)
|
||||||
&& !this.isChargingMove()
|
&& !this.isChargingMove()
|
||||||
@ -2104,7 +2104,7 @@ export class FlameBurstAttr extends MoveEffectAttr {
|
|||||||
const targetAlly = target.getAlly();
|
const targetAlly = target.getAlly();
|
||||||
const cancelled = new BooleanHolder(false);
|
const cancelled = new BooleanHolder(false);
|
||||||
|
|
||||||
if (!isNullOrUndefined(targetAlly)) {
|
if (targetAlly != null) {
|
||||||
applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: targetAlly, cancelled});
|
applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: targetAlly, cancelled});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2117,7 +2117,7 @@ export class FlameBurstAttr extends MoveEffectAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||||
return !isNullOrUndefined(target.getAlly()) ? -5 : 0;
|
return target.getAlly() != null ? -5 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3146,7 +3146,7 @@ export class WeatherInstantChargeAttr extends InstantChargeAttr {
|
|||||||
super((user, move) => {
|
super((user, move) => {
|
||||||
const currentWeather = globalScene.arena.weather;
|
const currentWeather = globalScene.arena.weather;
|
||||||
|
|
||||||
if (isNullOrUndefined(currentWeather?.weatherType)) {
|
if (currentWeather?.weatherType == null) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return !currentWeather?.isEffectSuppressed()
|
return !currentWeather?.isEffectSuppressed()
|
||||||
@ -6311,7 +6311,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
|
|||||||
pokemon.heal(Math.min(toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp()));
|
pokemon.heal(Math.min(toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp()));
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: getPokemonNameWithAffix(pokemon) }), 0, true);
|
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: getPokemonNameWithAffix(pokemon) }), 0, true);
|
||||||
const allyPokemon = user.getAlly();
|
const allyPokemon = user.getAlly();
|
||||||
if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1 && !isNullOrUndefined(allyPokemon)) {
|
if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1 && allyPokemon != null) {
|
||||||
// Handle cases where revived pokemon needs to get switched in on same turn
|
// Handle cases where revived pokemon needs to get switched in on same turn
|
||||||
if (allyPokemon.isFainted() || allyPokemon === pokemon) {
|
if (allyPokemon.isFainted() || allyPokemon === pokemon) {
|
||||||
// Enemy switch phase should be removed and replaced with the revived pkmn switching in
|
// Enemy switch phase should be removed and replaced with the revived pkmn switching in
|
||||||
@ -6480,7 +6480,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
|||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
|
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
|
||||||
|
|
||||||
// in double battles redirect potential moves off fled pokemon
|
// in double battles redirect potential moves off fled pokemon
|
||||||
if (globalScene.currentBattle.double && !isNullOrUndefined(allyPokemon)) {
|
if (globalScene.currentBattle.double && allyPokemon != null) {
|
||||||
globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon);
|
globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7140,7 +7140,7 @@ export class CopyMoveAttr extends CallMoveAttr {
|
|||||||
getCondition(): MoveConditionFunc {
|
getCondition(): MoveConditionFunc {
|
||||||
return (_user, target, _move) => {
|
return (_user, target, _move) => {
|
||||||
const lastMove = this.mirrorMove ? target.getLastNonVirtualMove(false, false)?.move : globalScene.currentBattle.lastMove;
|
const lastMove = this.mirrorMove ? target.getLastNonVirtualMove(false, false)?.move : globalScene.currentBattle.lastMove;
|
||||||
return !isNullOrUndefined(lastMove) && !this.invalidMoves.has(lastMove);
|
return lastMove != null && !this.invalidMoves.has(lastMove);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7149,10 +7149,11 @@ export class CopyMoveAttr extends CallMoveAttr {
|
|||||||
* Attribute used for moves that cause the target to repeat their last used move.
|
* Attribute used for moves that cause the target to repeat their last used move.
|
||||||
*
|
*
|
||||||
* Used by {@linkcode MoveId.INSTRUCT | Instruct}.
|
* Used by {@linkcode MoveId.INSTRUCT | Instruct}.
|
||||||
* @see [Instruct on Bulbapedia](https://bulbapedia.bulbagarden.net/wiki/Instruct_(move))
|
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Instruct_(move) | Instruct on Bulbapedia}
|
||||||
*/
|
*/
|
||||||
export class RepeatMoveAttr extends MoveEffectAttr {
|
export class RepeatMoveAttr extends MoveEffectAttr {
|
||||||
private movesetMove: PokemonMove;
|
private movesetMove: PokemonMove;
|
||||||
|
private lastMoveTargets: BattlerIndex[];
|
||||||
constructor() {
|
constructor() {
|
||||||
super(false, { trigger: MoveEffectTrigger.POST_APPLY }); // needed to ensure correct protect interaction
|
super(false, { trigger: MoveEffectTrigger.POST_APPLY }); // needed to ensure correct protect interaction
|
||||||
}
|
}
|
||||||
@ -7164,16 +7165,14 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
|||||||
* @returns `true` if the move succeeds
|
* @returns `true` if the move succeeds
|
||||||
*/
|
*/
|
||||||
apply(user: Pokemon, target: Pokemon): boolean {
|
apply(user: Pokemon, target: Pokemon): boolean {
|
||||||
// get the last move used (excluding status based failures) as well as the corresponding moveset slot
|
|
||||||
// bangs are justified as Instruct fails if no prior move or moveset move exists
|
|
||||||
// TODO: How does instruct work when copying a move called via Copycat that the user itself knows?
|
|
||||||
const lastMove = target.getLastNonVirtualMove()!;
|
|
||||||
|
|
||||||
// If the last move used can hit more than one target or has variable targets,
|
// If the last move used can hit more than one target or has variable targets,
|
||||||
// re-compute the targets for the attack (mainly for alternating double/single battles)
|
// re-compute the targets for the attack (mainly for alternating double/single battles)
|
||||||
// Rampaging moves (e.g. Outrage) are not included due to being incompatible with Instruct,
|
// Rampaging moves (e.g. Outrage) are not included due to being incompatible with Instruct,
|
||||||
// nor is Dragon Darts (due to its smart targeting bypassing normal target selection)
|
// nor is Dragon Darts (due to its smart targeting bypassing normal target selection)
|
||||||
let moveTargets = this.movesetMove.getMove().isMultiTarget() ? getMoveTargets(target, this.movesetMove.moveId).targets : lastMove.targets;
|
// TODO: Revisit this once target computation is moved to mid-use
|
||||||
|
let moveTargets = this.movesetMove.getMove().isMultiTarget()
|
||||||
|
? getMoveTargets(target, this.movesetMove.moveId).targets
|
||||||
|
: this.lastMoveTargets;
|
||||||
|
|
||||||
// In the event the instructed move's only target is a fainted opponent, redirect it to an alive ally if possible.
|
// In the event the instructed move's only target is a fainted opponent, redirect it to an alive ally if possible.
|
||||||
// Normally, all yet-unexecuted move phases would swap targets after any foe faints or flees (see `redirectPokemonMoves` in `battle-scene.ts`),
|
// Normally, all yet-unexecuted move phases would swap targets after any foe faints or flees (see `redirectPokemonMoves` in `battle-scene.ts`),
|
||||||
@ -7186,15 +7185,16 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
|||||||
&& firstTarget !== target.getAlly()
|
&& firstTarget !== target.getAlly()
|
||||||
) {
|
) {
|
||||||
const ally = firstTarget.getAlly();
|
const ally = firstTarget.getAlly();
|
||||||
if (!isNullOrUndefined(ally) && ally.isActive()) {
|
if (ally != null && ally.isActive()) {
|
||||||
moveTargets = [ ally.getBattlerIndex() ];
|
moveTargets = [ ally.getBattlerIndex() ];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the target is currently affected by Encore, increase its duration by 1 (to offset decrease during move use)
|
// If the target is currently affected by Encore, increase its duration by 1 (to offset decrease during move use)
|
||||||
|
// TODO: There might be a better way of doing this...
|
||||||
const targetEncore = target.getTag(BattlerTagType.ENCORE) as EncoreTag | undefined;
|
const targetEncore = target.getTag(BattlerTagType.ENCORE) as EncoreTag | undefined;
|
||||||
if (targetEncore) {
|
if (targetEncore) {
|
||||||
targetEncore.turnCount++
|
targetEncore["turnCount"]++
|
||||||
}
|
}
|
||||||
|
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:instructingMove", {
|
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:instructingMove", {
|
||||||
@ -7209,6 +7209,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
|||||||
getCondition(): MoveConditionFunc {
|
getCondition(): MoveConditionFunc {
|
||||||
return (_user, target, _move) => {
|
return (_user, target, _move) => {
|
||||||
// TODO: Check instruct behavior with struggle - ignore, fail or success
|
// TODO: Check instruct behavior with struggle - ignore, fail or success
|
||||||
|
// TODO: How does instruct work when copying a move called via Copycat that the user itself knows?
|
||||||
const lastMove = target.getLastNonVirtualMove();
|
const lastMove = target.getLastNonVirtualMove();
|
||||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move);
|
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move);
|
||||||
|
|
||||||
@ -7221,6 +7222,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.movesetMove = movesetMove;
|
this.movesetMove = movesetMove;
|
||||||
|
this.lastMoveTargets = lastMove.targets
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -7435,7 +7437,7 @@ export class SketchAttr extends MoveEffectAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const targetMove = target.getLastNonVirtualMove();
|
const targetMove = target.getLastNonVirtualMove();
|
||||||
return !isNullOrUndefined(targetMove)
|
return targetMove != null
|
||||||
&& !invalidSketchMoves.has(targetMove.move)
|
&& !invalidSketchMoves.has(targetMove.move)
|
||||||
&& user.getMoveset().every(m => m.moveId !== targetMove.move)
|
&& user.getMoveset().every(m => m.moveId !== targetMove.move)
|
||||||
};
|
};
|
||||||
@ -7492,7 +7494,7 @@ export class AbilityCopyAttr extends MoveEffectAttr {
|
|||||||
user.setTempAbility(target.getAbility());
|
user.setTempAbility(target.getAbility());
|
||||||
const ally = user.getAlly();
|
const ally = user.getAlly();
|
||||||
|
|
||||||
if (this.copyToPartner && globalScene.currentBattle?.double && !isNullOrUndefined(ally) && ally.hp) { // TODO is this the best way to check that the ally is active?
|
if (this.copyToPartner && globalScene.currentBattle?.double && ally != null && ally.hp) { // TODO is this the best way to check that the ally is active?
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", { pokemonName: getPokemonNameWithAffix(ally), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name }));
|
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", { pokemonName: getPokemonNameWithAffix(ally), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name }));
|
||||||
ally.setTempAbility(target.getAbility());
|
ally.setTempAbility(target.getAbility());
|
||||||
}
|
}
|
||||||
@ -8013,7 +8015,7 @@ const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Poke
|
|||||||
const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.getHeldItems().filter(i => i.isTransferable)?.length > 0;
|
const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.getHeldItems().filter(i => i.isTransferable)?.length > 0;
|
||||||
|
|
||||||
const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||||
if (isNullOrUndefined(target)) { // Fix bug when used against targets that have both fainted
|
if (target == null) { // Fix bug when used against targets that have both fainted
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
const heldItems = target.getHeldItems().filter(i => i.isTransferable);
|
const heldItems = target.getHeldItems().filter(i => i.isTransferable);
|
||||||
@ -8570,7 +8572,7 @@ export function initMoves() {
|
|||||||
.attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true)
|
.attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true)
|
||||||
.condition((_user, target, _move) => {
|
.condition((_user, target, _move) => {
|
||||||
const lastNonVirtualMove = target.getLastNonVirtualMove();
|
const lastNonVirtualMove = target.getLastNonVirtualMove();
|
||||||
return !isNullOrUndefined(lastNonVirtualMove) && lastNonVirtualMove.move !== MoveId.STRUGGLE;
|
return lastNonVirtualMove != null && lastNonVirtualMove.move !== MoveId.STRUGGLE;
|
||||||
})
|
})
|
||||||
.ignoresSubstitute()
|
.ignoresSubstitute()
|
||||||
.reflectable(),
|
.reflectable(),
|
||||||
@ -9912,7 +9914,7 @@ export function initMoves() {
|
|||||||
.condition(failOnGravityCondition)
|
.condition(failOnGravityCondition)
|
||||||
.condition((_user, target, _move) => ![ SpeciesId.DIGLETT, SpeciesId.DUGTRIO, SpeciesId.ALOLA_DIGLETT, SpeciesId.ALOLA_DUGTRIO, SpeciesId.SANDYGAST, SpeciesId.PALOSSAND, SpeciesId.WIGLETT, SpeciesId.WUGTRIO ].includes(target.species.speciesId))
|
.condition((_user, target, _move) => ![ SpeciesId.DIGLETT, SpeciesId.DUGTRIO, SpeciesId.ALOLA_DIGLETT, SpeciesId.ALOLA_DUGTRIO, SpeciesId.SANDYGAST, SpeciesId.PALOSSAND, SpeciesId.WIGLETT, SpeciesId.WUGTRIO ].includes(target.species.speciesId))
|
||||||
.condition((_user, target, _move) => !(target.species.speciesId === SpeciesId.GENGAR && target.getFormKey() === "mega"))
|
.condition((_user, target, _move) => !(target.species.speciesId === SpeciesId.GENGAR && target.getFormKey() === "mega"))
|
||||||
.condition((_user, target, _move) => isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING)))
|
.condition((_user, target, _move) => target.getTag(BattlerTagType.INGRAIN) == null && target.getTag(BattlerTagType.IGNORE_FLYING) == null)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3)
|
.attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3)
|
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3)
|
||||||
.reflectable(),
|
.reflectable(),
|
||||||
|
@ -48,7 +48,7 @@ import { getRandomPartyMemberFunc, trainerConfigs } from "#trainers/trainer-conf
|
|||||||
import { TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#trainers/trainer-party-template";
|
import { TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#trainers/trainer-party-template";
|
||||||
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
||||||
import { MoveInfoOverlay } from "#ui/move-info-overlay";
|
import { MoveInfoOverlay } from "#ui/move-info-overlay";
|
||||||
import { isNullOrUndefined, randSeedInt, randSeedShuffle } from "#utils/common";
|
import { randSeedInt, randSeedShuffle } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
/** the i18n namespace for the encounter */
|
/** the i18n namespace for the encounter */
|
||||||
@ -571,7 +571,7 @@ function getTrainerConfigForWave(waveIndex: number) {
|
|||||||
.setPartyMemberFunc(
|
.setPartyMemberFunc(
|
||||||
4,
|
4,
|
||||||
getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => {
|
||||||
if (!isNullOrUndefined(pool3Mon.formIndex)) {
|
if (pool3Mon.formIndex != null) {
|
||||||
p.formIndex = pool3Mon.formIndex;
|
p.formIndex = pool3Mon.formIndex;
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
p.generateName();
|
p.generateName();
|
||||||
@ -603,7 +603,7 @@ function getTrainerConfigForWave(waveIndex: number) {
|
|||||||
.setPartyMemberFunc(
|
.setPartyMemberFunc(
|
||||||
3,
|
3,
|
||||||
getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => {
|
||||||
if (!isNullOrUndefined(pool3Mon.formIndex)) {
|
if (pool3Mon.formIndex != null) {
|
||||||
p.formIndex = pool3Mon.formIndex;
|
p.formIndex = pool3Mon.formIndex;
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
p.generateName();
|
p.generateName();
|
||||||
@ -613,7 +613,7 @@ function getTrainerConfigForWave(waveIndex: number) {
|
|||||||
.setPartyMemberFunc(
|
.setPartyMemberFunc(
|
||||||
4,
|
4,
|
||||||
getRandomPartyMemberFunc([pool3Mon2.species], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([pool3Mon2.species], TrainerSlot.TRAINER, true, p => {
|
||||||
if (!isNullOrUndefined(pool3Mon2.formIndex)) {
|
if (pool3Mon2.formIndex != null) {
|
||||||
p.formIndex = pool3Mon2.formIndex;
|
p.formIndex = pool3Mon2.formIndex;
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
p.generateName();
|
p.generateName();
|
||||||
@ -648,7 +648,7 @@ function getTrainerConfigForWave(waveIndex: number) {
|
|||||||
.setPartyMemberFunc(
|
.setPartyMemberFunc(
|
||||||
3,
|
3,
|
||||||
getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => {
|
||||||
if (!isNullOrUndefined(pool3Mon.formIndex)) {
|
if (pool3Mon.formIndex != null) {
|
||||||
p.formIndex = pool3Mon.formIndex;
|
p.formIndex = pool3Mon.formIndex;
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
p.generateName();
|
p.generateName();
|
||||||
@ -687,7 +687,7 @@ function getTrainerConfigForWave(waveIndex: number) {
|
|||||||
.setPartyMemberFunc(
|
.setPartyMemberFunc(
|
||||||
2,
|
2,
|
||||||
getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => {
|
||||||
if (!isNullOrUndefined(pool3Mon.formIndex)) {
|
if (pool3Mon.formIndex != null) {
|
||||||
p.formIndex = pool3Mon.formIndex;
|
p.formIndex = pool3Mon.formIndex;
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
p.generateName();
|
p.generateName();
|
||||||
@ -697,7 +697,7 @@ function getTrainerConfigForWave(waveIndex: number) {
|
|||||||
.setPartyMemberFunc(
|
.setPartyMemberFunc(
|
||||||
3,
|
3,
|
||||||
getRandomPartyMemberFunc([pool3Mon2.species], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([pool3Mon2.species], TrainerSlot.TRAINER, true, p => {
|
||||||
if (!isNullOrUndefined(pool3Mon2.formIndex)) {
|
if (pool3Mon2.formIndex != null) {
|
||||||
p.formIndex = pool3Mon2.formIndex;
|
p.formIndex = pool3Mon2.formIndex;
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
p.generateName();
|
p.generateName();
|
||||||
|
@ -15,7 +15,7 @@ import { getRandomPlayerPokemon, getRandomSpeciesByStarterCost } from "#mystery-
|
|||||||
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
|
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
|
||||||
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
|
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
|
||||||
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
|
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
|
||||||
import { isNullOrUndefined, randSeedInt } from "#utils/common";
|
import { randSeedInt } from "#utils/common";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
|
|
||||||
/** i18n namespace for encounter */
|
/** i18n namespace for encounter */
|
||||||
@ -192,7 +192,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
if (!isNullOrUndefined(bossSpecies.forms) && bossSpecies.forms.length > 0) {
|
if (bossSpecies.forms != null && bossSpecies.forms.length > 0) {
|
||||||
pokemonConfig.formIndex = 0;
|
pokemonConfig.formIndex = 0;
|
||||||
}
|
}
|
||||||
const config: EnemyPartyConfig = {
|
const config: EnemyPartyConfig = {
|
||||||
|
@ -45,7 +45,7 @@ import {
|
|||||||
TypeRequirement,
|
TypeRequirement,
|
||||||
} from "#mystery-encounters/mystery-encounter-requirements";
|
} from "#mystery-encounters/mystery-encounter-requirements";
|
||||||
import { FIRE_RESISTANT_ABILITIES } from "#mystery-encounters/requirement-groups";
|
import { FIRE_RESISTANT_ABILITIES } from "#mystery-encounters/requirement-groups";
|
||||||
import { isNullOrUndefined, randSeedInt } from "#utils/common";
|
import { randSeedInt } from "#utils/common";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
|
|
||||||
/** the i18n namespace for the encounter */
|
/** the i18n namespace for the encounter */
|
||||||
@ -238,7 +238,7 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w
|
|||||||
|
|
||||||
// Burn random member
|
// Burn random member
|
||||||
const burnable = nonFireTypes.filter(
|
const burnable = nonFireTypes.filter(
|
||||||
p => isNullOrUndefined(p.status) || isNullOrUndefined(p.status.effect) || p.status.effect === StatusEffect.NONE,
|
p => p.status == null || p.status.effect == null || p.status.effect === StatusEffect.NONE,
|
||||||
);
|
);
|
||||||
if (burnable?.length > 0) {
|
if (burnable?.length > 0) {
|
||||||
const roll = randSeedInt(burnable.length);
|
const roll = randSeedInt(burnable.length);
|
||||||
|
@ -43,7 +43,7 @@ import { PartySizeRequirement } from "#mystery-encounters/mystery-encounter-requ
|
|||||||
import { PokemonData } from "#system/pokemon-data";
|
import { PokemonData } from "#system/pokemon-data";
|
||||||
import { MusicPreference } from "#system/settings";
|
import { MusicPreference } from "#system/settings";
|
||||||
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
||||||
import { isNullOrUndefined, NumberHolder, randInt, randSeedInt, randSeedItem, randSeedShuffle } from "#utils/common";
|
import { NumberHolder, randInt, randSeedInt, randSeedItem, randSeedShuffle } from "#utils/common";
|
||||||
import { getEnumKeys } from "#utils/enums";
|
import { getEnumKeys } from "#utils/enums";
|
||||||
import { getRandomLocaleEntry } from "#utils/i18n";
|
import { getRandomLocaleEntry } from "#utils/i18n";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
@ -537,7 +537,7 @@ function generateTradeOption(alreadyUsedSpecies: PokemonSpecies[], originalBst?:
|
|||||||
bstCap = originalBst + 100;
|
bstCap = originalBst + 100;
|
||||||
bstMin = originalBst - 100;
|
bstMin = originalBst - 100;
|
||||||
}
|
}
|
||||||
while (isNullOrUndefined(newSpecies)) {
|
while (newSpecies == null) {
|
||||||
// Get all non-legendary species that fall within the Bst range requirements
|
// Get all non-legendary species that fall within the Bst range requirements
|
||||||
let validSpecies = allSpecies.filter(s => {
|
let validSpecies = allSpecies.filter(s => {
|
||||||
const isLegendaryOrMythical = s.legendary || s.subLegendary || s.mythical;
|
const isLegendaryOrMythical = s.legendary || s.subLegendary || s.mythical;
|
||||||
@ -550,7 +550,7 @@ function generateTradeOption(alreadyUsedSpecies: PokemonSpecies[], originalBst?:
|
|||||||
if (validSpecies?.length > 20) {
|
if (validSpecies?.length > 20) {
|
||||||
validSpecies = randSeedShuffle(validSpecies);
|
validSpecies = randSeedShuffle(validSpecies);
|
||||||
newSpecies = validSpecies.pop();
|
newSpecies = validSpecies.pop();
|
||||||
while (isNullOrUndefined(newSpecies) || alreadyUsedSpecies.includes(newSpecies)) {
|
while (newSpecies == null || alreadyUsedSpecies.includes(newSpecies)) {
|
||||||
newSpecies = validSpecies.pop();
|
newSpecies = validSpecies.pop();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -28,7 +28,7 @@ import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
|
|||||||
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
|
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
|
||||||
import { MoneyRequirement } from "#mystery-encounters/mystery-encounter-requirements";
|
import { MoneyRequirement } from "#mystery-encounters/mystery-encounter-requirements";
|
||||||
import { PokemonData } from "#system/pokemon-data";
|
import { PokemonData } from "#system/pokemon-data";
|
||||||
import { isNullOrUndefined, randSeedInt, randSeedItem } from "#utils/common";
|
import { randSeedInt, randSeedItem } from "#utils/common";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
|
|
||||||
/** the i18n namespace for this encounter */
|
/** the i18n namespace for this encounter */
|
||||||
@ -81,7 +81,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
|
|||||||
let tries = 0;
|
let tries = 0;
|
||||||
|
|
||||||
// Reroll any species that don't have HAs
|
// Reroll any species that don't have HAs
|
||||||
while ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === AbilityId.NONE) && tries < 5) {
|
while ((species.abilityHidden == null || species.abilityHidden === AbilityId.NONE) && tries < 5) {
|
||||||
species = getSalesmanSpeciesOffer();
|
species = getSalesmanSpeciesOffer();
|
||||||
tries++;
|
tries++;
|
||||||
}
|
}
|
||||||
@ -110,7 +110,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
|
|||||||
*/
|
*/
|
||||||
if (
|
if (
|
||||||
r === 0
|
r === 0
|
||||||
|| ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === AbilityId.NONE)
|
|| ((species.abilityHidden == null || species.abilityHidden === AbilityId.NONE)
|
||||||
&& validEventEncounters.length === 0)
|
&& validEventEncounters.length === 0)
|
||||||
) {
|
) {
|
||||||
// If you roll 1%, give shiny Magikarp with random variant
|
// If you roll 1%, give shiny Magikarp with random variant
|
||||||
@ -118,7 +118,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
|
|||||||
pokemon = new PlayerPokemon(species, 5, 2, undefined, undefined, true);
|
pokemon = new PlayerPokemon(species, 5, 2, undefined, undefined, true);
|
||||||
} else if (
|
} else if (
|
||||||
validEventEncounters.length > 0
|
validEventEncounters.length > 0
|
||||||
&& (r <= EVENT_THRESHOLD || isNullOrUndefined(species.abilityHidden) || species.abilityHidden === AbilityId.NONE)
|
&& (r <= EVENT_THRESHOLD || species.abilityHidden == null || species.abilityHidden === AbilityId.NONE)
|
||||||
) {
|
) {
|
||||||
tries = 0;
|
tries = 0;
|
||||||
do {
|
do {
|
||||||
|
@ -28,7 +28,7 @@ import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encou
|
|||||||
import { PokemonData } from "#system/pokemon-data";
|
import { PokemonData } from "#system/pokemon-data";
|
||||||
import type { HeldModifierConfig } from "#types/held-modifier-config";
|
import type { HeldModifierConfig } from "#types/held-modifier-config";
|
||||||
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
||||||
import { isNullOrUndefined, randSeedShuffle } from "#utils/common";
|
import { randSeedShuffle } from "#utils/common";
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
@ -324,7 +324,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde
|
|||||||
// Only update the fusion's dex data if the Pokemon is already caught in dex (ignore rentals)
|
// Only update the fusion's dex data if the Pokemon is already caught in dex (ignore rentals)
|
||||||
const rootFusionSpecies = playerPokemon.fusionSpecies?.getRootSpeciesId();
|
const rootFusionSpecies = playerPokemon.fusionSpecies?.getRootSpeciesId();
|
||||||
if (
|
if (
|
||||||
!isNullOrUndefined(rootFusionSpecies)
|
rootFusionSpecies != null
|
||||||
&& speciesStarterCosts.hasOwnProperty(rootFusionSpecies)
|
&& speciesStarterCosts.hasOwnProperty(rootFusionSpecies)
|
||||||
&& !!globalScene.gameData.dexData[rootFusionSpecies].caughtAttr
|
&& !!globalScene.gameData.dexData[rootFusionSpecies].caughtAttr
|
||||||
) {
|
) {
|
||||||
|
@ -32,7 +32,7 @@ import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encou
|
|||||||
import { MoveRequirement, PersistentModifierRequirement } from "#mystery-encounters/mystery-encounter-requirements";
|
import { MoveRequirement, PersistentModifierRequirement } from "#mystery-encounters/mystery-encounter-requirements";
|
||||||
import { CHARMING_MOVES } from "#mystery-encounters/requirement-groups";
|
import { CHARMING_MOVES } from "#mystery-encounters/requirement-groups";
|
||||||
import { PokemonData } from "#system/pokemon-data";
|
import { PokemonData } from "#system/pokemon-data";
|
||||||
import { isNullOrUndefined, randSeedInt } from "#utils/common";
|
import { randSeedInt } from "#utils/common";
|
||||||
|
|
||||||
/** the i18n namespace for the encounter */
|
/** the i18n namespace for the encounter */
|
||||||
const namespace = "mysteryEncounters/uncommonBreed";
|
const namespace = "mysteryEncounters/uncommonBreed";
|
||||||
@ -167,7 +167,7 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder.
|
|||||||
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
||||||
|
|
||||||
const eggMove = encounter.misc.eggMove;
|
const eggMove = encounter.misc.eggMove;
|
||||||
if (!isNullOrUndefined(eggMove)) {
|
if (eggMove != null) {
|
||||||
// Check what type of move the egg move is to determine target
|
// Check what type of move the egg move is to determine target
|
||||||
const pokemonMove = new PokemonMove(eggMove);
|
const pokemonMove = new PokemonMove(eggMove);
|
||||||
const move = pokemonMove.getMove();
|
const move = pokemonMove.getMove();
|
||||||
|
@ -41,7 +41,7 @@ import { PokemonData } from "#system/pokemon-data";
|
|||||||
import { trainerConfigs } from "#trainers/trainer-config";
|
import { trainerConfigs } from "#trainers/trainer-config";
|
||||||
import { TrainerPartyTemplate } from "#trainers/trainer-party-template";
|
import { TrainerPartyTemplate } from "#trainers/trainer-party-template";
|
||||||
import type { HeldModifierConfig } from "#types/held-modifier-config";
|
import type { HeldModifierConfig } from "#types/held-modifier-config";
|
||||||
import { isNullOrUndefined, NumberHolder, randSeedInt, randSeedShuffle } from "#utils/common";
|
import { NumberHolder, randSeedInt, randSeedShuffle } from "#utils/common";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
|
|
||||||
/** i18n namespace for encounter */
|
/** i18n namespace for encounter */
|
||||||
@ -634,7 +634,7 @@ function getTransformedSpecies(
|
|||||||
alreadyUsedSpecies: PokemonSpecies[],
|
alreadyUsedSpecies: PokemonSpecies[],
|
||||||
): PokemonSpecies {
|
): PokemonSpecies {
|
||||||
let newSpecies: PokemonSpecies | undefined;
|
let newSpecies: PokemonSpecies | undefined;
|
||||||
while (isNullOrUndefined(newSpecies)) {
|
while (newSpecies == null) {
|
||||||
const bstCap = originalBst + bstSearchRange[1];
|
const bstCap = originalBst + bstSearchRange[1];
|
||||||
const bstMin = Math.max(originalBst + bstSearchRange[0], 0);
|
const bstMin = Math.max(originalBst + bstSearchRange[0], 0);
|
||||||
|
|
||||||
@ -655,7 +655,7 @@ function getTransformedSpecies(
|
|||||||
if (validSpecies?.length > 20) {
|
if (validSpecies?.length > 20) {
|
||||||
validSpecies = randSeedShuffle(validSpecies);
|
validSpecies = randSeedShuffle(validSpecies);
|
||||||
newSpecies = validSpecies.pop();
|
newSpecies = validSpecies.pop();
|
||||||
while (isNullOrUndefined(newSpecies) || alreadyUsedSpecies.includes(newSpecies)) {
|
while (newSpecies == null || alreadyUsedSpecies.includes(newSpecies)) {
|
||||||
newSpecies = validSpecies.pop();
|
newSpecies = validSpecies.pop();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -771,12 +771,12 @@ async function addEggMoveToNewPokemonMoveset(
|
|||||||
if (eggMoves) {
|
if (eggMoves) {
|
||||||
const eggMoveIndices = randSeedShuffle([0, 1, 2, 3]);
|
const eggMoveIndices = randSeedShuffle([0, 1, 2, 3]);
|
||||||
let randomEggMoveIndex = eggMoveIndices.pop();
|
let randomEggMoveIndex = eggMoveIndices.pop();
|
||||||
let randomEggMove = !isNullOrUndefined(randomEggMoveIndex) ? eggMoves[randomEggMoveIndex] : null;
|
let randomEggMove = randomEggMoveIndex != null ? eggMoves[randomEggMoveIndex] : null;
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
while (retries < 3 && (!randomEggMove || newPokemon.moveset.some(m => m.moveId === randomEggMove))) {
|
while (retries < 3 && (!randomEggMove || newPokemon.moveset.some(m => m.moveId === randomEggMove))) {
|
||||||
// If Pokemon already knows this move, roll for another egg move
|
// If Pokemon already knows this move, roll for another egg move
|
||||||
randomEggMoveIndex = eggMoveIndices.pop();
|
randomEggMoveIndex = eggMoveIndices.pop();
|
||||||
randomEggMove = !isNullOrUndefined(randomEggMoveIndex) ? eggMoves[randomEggMoveIndex] : null;
|
randomEggMove = randomEggMoveIndex != null ? eggMoves[randomEggMoveIndex] : null;
|
||||||
retries++;
|
retries++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -791,11 +791,7 @@ async function addEggMoveToNewPokemonMoveset(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For pokemon that the player owns (including ones just caught), unlock the egg move
|
// For pokemon that the player owns (including ones just caught), unlock the egg move
|
||||||
if (
|
if (!forBattle && randomEggMoveIndex != null && !!globalScene.gameData.dexData[speciesRootForm].caughtAttr) {
|
||||||
!forBattle
|
|
||||||
&& !isNullOrUndefined(randomEggMoveIndex)
|
|
||||||
&& !!globalScene.gameData.dexData[speciesRootForm].caughtAttr
|
|
||||||
) {
|
|
||||||
await globalScene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), randomEggMoveIndex, true);
|
await globalScene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), randomEggMoveIndex, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
MoneyRequirement,
|
MoneyRequirement,
|
||||||
TypeRequirement,
|
TypeRequirement,
|
||||||
} from "#mystery-encounters/mystery-encounter-requirements";
|
} from "#mystery-encounters/mystery-encounter-requirements";
|
||||||
import { isNullOrUndefined, randSeedInt } from "#utils/common";
|
import { randSeedInt } from "#utils/common";
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noConfusingVoidType: void unions in callbacks are OK
|
// biome-ignore lint/suspicious/noConfusingVoidType: void unions in callbacks are OK
|
||||||
export type OptionPhaseCallback = () => Promise<void | boolean>;
|
export type OptionPhaseCallback = () => Promise<void | boolean>;
|
||||||
@ -62,7 +62,7 @@ export class MysteryEncounterOption implements IMysteryEncounterOption {
|
|||||||
onPostOptionPhase?: OptionPhaseCallback;
|
onPostOptionPhase?: OptionPhaseCallback;
|
||||||
|
|
||||||
constructor(option: IMysteryEncounterOption | null) {
|
constructor(option: IMysteryEncounterOption | null) {
|
||||||
if (!isNullOrUndefined(option)) {
|
if (option != null) {
|
||||||
Object.assign(this, option);
|
Object.assign(this, option);
|
||||||
}
|
}
|
||||||
this.hasDexProgress = this.hasDexProgress ?? false;
|
this.hasDexProgress = this.hasDexProgress ?? false;
|
||||||
|
@ -15,7 +15,7 @@ import { WeatherType } from "#enums/weather-type";
|
|||||||
import type { PlayerPokemon } from "#field/pokemon";
|
import type { PlayerPokemon } from "#field/pokemon";
|
||||||
import { AttackTypeBoosterModifier } from "#modifiers/modifier";
|
import { AttackTypeBoosterModifier } from "#modifiers/modifier";
|
||||||
import type { AttackTypeBoosterModifierType } from "#modifiers/modifier-type";
|
import type { AttackTypeBoosterModifierType } from "#modifiers/modifier-type";
|
||||||
import { coerceArray, isNullOrUndefined } from "#utils/common";
|
import { coerceArray } from "#utils/common";
|
||||||
|
|
||||||
export interface EncounterRequirement {
|
export interface EncounterRequirement {
|
||||||
meetsRequirement(): boolean; // Boolean to see if a requirement is met
|
meetsRequirement(): boolean; // Boolean to see if a requirement is met
|
||||||
@ -219,7 +219,7 @@ export class WaveRangeRequirement extends EncounterSceneRequirement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
if (!isNullOrUndefined(this.waveRange) && this.waveRange[0] <= this.waveRange[1]) {
|
if (this.waveRange != null && this.waveRange[0] <= this.waveRange[1]) {
|
||||||
const waveIndex = globalScene.currentBattle.waveIndex;
|
const waveIndex = globalScene.currentBattle.waveIndex;
|
||||||
if (
|
if (
|
||||||
(waveIndex >= 0 && this.waveRange[0] >= 0 && this.waveRange[0] > waveIndex)
|
(waveIndex >= 0 && this.waveRange[0] >= 0 && this.waveRange[0] > waveIndex)
|
||||||
@ -275,11 +275,7 @@ export class TimeOfDayRequirement extends EncounterSceneRequirement {
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
const timeOfDay = globalScene.arena?.getTimeOfDay();
|
const timeOfDay = globalScene.arena?.getTimeOfDay();
|
||||||
return !(
|
return !(timeOfDay != null && this.requiredTimeOfDay?.length > 0 && !this.requiredTimeOfDay.includes(timeOfDay));
|
||||||
!isNullOrUndefined(timeOfDay)
|
|
||||||
&& this.requiredTimeOfDay?.length > 0
|
|
||||||
&& !this.requiredTimeOfDay.includes(timeOfDay)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override getDialogueToken(_pokemon?: PlayerPokemon): [string, string] {
|
override getDialogueToken(_pokemon?: PlayerPokemon): [string, string] {
|
||||||
@ -298,7 +294,7 @@ export class WeatherRequirement extends EncounterSceneRequirement {
|
|||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
const currentWeather = globalScene.arena.weather?.weatherType;
|
const currentWeather = globalScene.arena.weather?.weatherType;
|
||||||
return !(
|
return !(
|
||||||
!isNullOrUndefined(currentWeather)
|
currentWeather != null
|
||||||
&& this.requiredWeather?.length > 0
|
&& this.requiredWeather?.length > 0
|
||||||
&& !this.requiredWeather.includes(currentWeather!)
|
&& !this.requiredWeather.includes(currentWeather!)
|
||||||
);
|
);
|
||||||
@ -307,7 +303,7 @@ export class WeatherRequirement extends EncounterSceneRequirement {
|
|||||||
override getDialogueToken(_pokemon?: PlayerPokemon): [string, string] {
|
override getDialogueToken(_pokemon?: PlayerPokemon): [string, string] {
|
||||||
const currentWeather = globalScene.arena.weather?.weatherType;
|
const currentWeather = globalScene.arena.weather?.weatherType;
|
||||||
let token = "";
|
let token = "";
|
||||||
if (!isNullOrUndefined(currentWeather)) {
|
if (currentWeather != null) {
|
||||||
token = WeatherType[currentWeather].replace("_", " ").toLocaleLowerCase();
|
token = WeatherType[currentWeather].replace("_", " ").toLocaleLowerCase();
|
||||||
}
|
}
|
||||||
return ["weather", token];
|
return ["weather", token];
|
||||||
@ -331,7 +327,7 @@ export class PartySizeRequirement extends EncounterSceneRequirement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
if (!isNullOrUndefined(this.partySizeRange) && this.partySizeRange[0] <= this.partySizeRange[1]) {
|
if (this.partySizeRange != null && this.partySizeRange[0] <= this.partySizeRange[1]) {
|
||||||
const partySize = this.excludeDisallowedPokemon
|
const partySize = this.excludeDisallowedPokemon
|
||||||
? globalScene.getPokemonAllowedInBattle().length
|
? globalScene.getPokemonAllowedInBattle().length
|
||||||
: globalScene.getPlayerParty().length;
|
: globalScene.getPlayerParty().length;
|
||||||
@ -363,7 +359,7 @@ export class PersistentModifierRequirement extends EncounterSceneRequirement {
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
const partyPokemon = globalScene.getPlayerParty();
|
const partyPokemon = globalScene.getPlayerParty();
|
||||||
if (isNullOrUndefined(partyPokemon) || this.requiredHeldItemModifiers?.length < 0) {
|
if (partyPokemon == null || this.requiredHeldItemModifiers?.length < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let modifierCount = 0;
|
let modifierCount = 0;
|
||||||
@ -396,7 +392,7 @@ export class MoneyRequirement extends EncounterSceneRequirement {
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
const money = globalScene.money;
|
const money = globalScene.money;
|
||||||
if (isNullOrUndefined(money)) {
|
if (money == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -429,7 +425,7 @@ export class SpeciesRequirement extends EncounterPokemonRequirement {
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
const partyPokemon = globalScene.getPlayerParty();
|
const partyPokemon = globalScene.getPlayerParty();
|
||||||
if (isNullOrUndefined(partyPokemon) || this.requiredSpecies?.length < 0) {
|
if (partyPokemon == null || this.requiredSpecies?.length < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||||
@ -469,7 +465,7 @@ export class NatureRequirement extends EncounterPokemonRequirement {
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
const partyPokemon = globalScene.getPlayerParty();
|
const partyPokemon = globalScene.getPlayerParty();
|
||||||
if (isNullOrUndefined(partyPokemon) || this.requiredNature?.length < 0) {
|
if (partyPokemon == null || this.requiredNature?.length < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||||
@ -484,7 +480,7 @@ export class NatureRequirement extends EncounterPokemonRequirement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override getDialogueToken(pokemon?: PlayerPokemon): [string, string] {
|
override getDialogueToken(pokemon?: PlayerPokemon): [string, string] {
|
||||||
if (!isNullOrUndefined(pokemon?.nature) && this.requiredNature.includes(pokemon.nature)) {
|
if (pokemon?.nature != null && this.requiredNature.includes(pokemon.nature)) {
|
||||||
return ["nature", Nature[pokemon.nature]];
|
return ["nature", Nature[pokemon.nature]];
|
||||||
}
|
}
|
||||||
return ["nature", ""];
|
return ["nature", ""];
|
||||||
@ -508,7 +504,7 @@ export class TypeRequirement extends EncounterPokemonRequirement {
|
|||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
let partyPokemon = globalScene.getPlayerParty();
|
let partyPokemon = globalScene.getPlayerParty();
|
||||||
|
|
||||||
if (isNullOrUndefined(partyPokemon)) {
|
if (partyPokemon == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -561,7 +557,7 @@ export class MoveRequirement extends EncounterPokemonRequirement {
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
const partyPokemon = globalScene.getPlayerParty();
|
const partyPokemon = globalScene.getPlayerParty();
|
||||||
if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) {
|
if (partyPokemon == null || this.requiredMoves?.length < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||||
@ -612,7 +608,7 @@ export class CompatibleMoveRequirement extends EncounterPokemonRequirement {
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
const partyPokemon = globalScene.getPlayerParty();
|
const partyPokemon = globalScene.getPlayerParty();
|
||||||
if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) {
|
if (partyPokemon == null || this.requiredMoves?.length < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||||
@ -668,7 +664,7 @@ export class AbilityRequirement extends EncounterPokemonRequirement {
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
const partyPokemon = globalScene.getPlayerParty();
|
const partyPokemon = globalScene.getPlayerParty();
|
||||||
if (isNullOrUndefined(partyPokemon) || this.requiredAbilities?.length < 0) {
|
if (partyPokemon == null || this.requiredAbilities?.length < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||||
@ -692,7 +688,7 @@ export class AbilityRequirement extends EncounterPokemonRequirement {
|
|||||||
|
|
||||||
override getDialogueToken(pokemon?: PlayerPokemon): [string, string] {
|
override getDialogueToken(pokemon?: PlayerPokemon): [string, string] {
|
||||||
const matchingAbility = this.requiredAbilities.find(a => pokemon?.hasAbility(a, false));
|
const matchingAbility = this.requiredAbilities.find(a => pokemon?.hasAbility(a, false));
|
||||||
if (!isNullOrUndefined(matchingAbility)) {
|
if (matchingAbility != null) {
|
||||||
return ["ability", allAbilities[matchingAbility].name];
|
return ["ability", allAbilities[matchingAbility].name];
|
||||||
}
|
}
|
||||||
return ["ability", ""];
|
return ["ability", ""];
|
||||||
@ -713,7 +709,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement {
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
const partyPokemon = globalScene.getPlayerParty();
|
const partyPokemon = globalScene.getPlayerParty();
|
||||||
if (isNullOrUndefined(partyPokemon) || this.requiredStatusEffect?.length < 0) {
|
if (partyPokemon == null || this.requiredStatusEffect?.length < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const x = this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
const x = this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||||
@ -727,11 +723,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement {
|
|||||||
return this.requiredStatusEffect.some(statusEffect => {
|
return this.requiredStatusEffect.some(statusEffect => {
|
||||||
if (statusEffect === StatusEffect.NONE) {
|
if (statusEffect === StatusEffect.NONE) {
|
||||||
// StatusEffect.NONE also checks for null or undefined status
|
// StatusEffect.NONE also checks for null or undefined status
|
||||||
return (
|
return pokemon.status == null || pokemon.status.effect == null || pokemon.status.effect === statusEffect;
|
||||||
isNullOrUndefined(pokemon.status)
|
|
||||||
|| isNullOrUndefined(pokemon.status.effect)
|
|
||||||
|| pokemon.status.effect === statusEffect
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return pokemon.status?.effect === statusEffect;
|
return pokemon.status?.effect === statusEffect;
|
||||||
});
|
});
|
||||||
@ -742,11 +734,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement {
|
|||||||
return !this.requiredStatusEffect.some(statusEffect => {
|
return !this.requiredStatusEffect.some(statusEffect => {
|
||||||
if (statusEffect === StatusEffect.NONE) {
|
if (statusEffect === StatusEffect.NONE) {
|
||||||
// StatusEffect.NONE also checks for null or undefined status
|
// StatusEffect.NONE also checks for null or undefined status
|
||||||
return (
|
return pokemon.status == null || pokemon.status.effect == null || pokemon.status.effect === statusEffect;
|
||||||
isNullOrUndefined(pokemon.status)
|
|
||||||
|| isNullOrUndefined(pokemon.status.effect)
|
|
||||||
|| pokemon.status.effect === statusEffect
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return pokemon.status?.effect === statusEffect;
|
return pokemon.status?.effect === statusEffect;
|
||||||
});
|
});
|
||||||
@ -756,9 +744,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement {
|
|||||||
override getDialogueToken(pokemon?: PlayerPokemon): [string, string] {
|
override getDialogueToken(pokemon?: PlayerPokemon): [string, string] {
|
||||||
const reqStatus = this.requiredStatusEffect.filter(a => {
|
const reqStatus = this.requiredStatusEffect.filter(a => {
|
||||||
if (a === StatusEffect.NONE) {
|
if (a === StatusEffect.NONE) {
|
||||||
return (
|
return pokemon?.status == null || pokemon.status.effect == null || pokemon.status.effect === a;
|
||||||
isNullOrUndefined(pokemon?.status) || isNullOrUndefined(pokemon.status.effect) || pokemon.status.effect === a
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return pokemon!.status?.effect === a;
|
return pokemon!.status?.effect === a;
|
||||||
});
|
});
|
||||||
@ -788,7 +774,7 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
const partyPokemon = globalScene.getPlayerParty();
|
const partyPokemon = globalScene.getPlayerParty();
|
||||||
if (isNullOrUndefined(partyPokemon) || this.requiredFormChangeItem?.length < 0) {
|
if (partyPokemon == null || this.requiredFormChangeItem?.length < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||||
@ -847,7 +833,7 @@ export class HeldItemRequirement extends EncounterPokemonRequirement {
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
const partyPokemon = globalScene.getPlayerParty();
|
const partyPokemon = globalScene.getPlayerParty();
|
||||||
if (isNullOrUndefined(partyPokemon)) {
|
if (partyPokemon == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||||
@ -911,7 +897,7 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
const partyPokemon = globalScene.getPlayerParty();
|
const partyPokemon = globalScene.getPlayerParty();
|
||||||
if (isNullOrUndefined(partyPokemon)) {
|
if (partyPokemon == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||||
@ -978,7 +964,7 @@ export class LevelRequirement extends EncounterPokemonRequirement {
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
// Party Pokemon inside required level range
|
// Party Pokemon inside required level range
|
||||||
if (!isNullOrUndefined(this.requiredLevelRange) && this.requiredLevelRange[0] <= this.requiredLevelRange[1]) {
|
if (this.requiredLevelRange != null && this.requiredLevelRange[0] <= this.requiredLevelRange[1]) {
|
||||||
const partyPokemon = globalScene.getPlayerParty();
|
const partyPokemon = globalScene.getPlayerParty();
|
||||||
const pokemonInRange = this.queryParty(partyPokemon);
|
const pokemonInRange = this.queryParty(partyPokemon);
|
||||||
if (pokemonInRange.length < this.minNumberOfPokemon) {
|
if (pokemonInRange.length < this.minNumberOfPokemon) {
|
||||||
@ -1019,10 +1005,7 @@ export class FriendshipRequirement extends EncounterPokemonRequirement {
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
// Party Pokemon inside required friendship range
|
// Party Pokemon inside required friendship range
|
||||||
if (
|
if (this.requiredFriendshipRange != null && this.requiredFriendshipRange[0] <= this.requiredFriendshipRange[1]) {
|
||||||
!isNullOrUndefined(this.requiredFriendshipRange)
|
|
||||||
&& this.requiredFriendshipRange[0] <= this.requiredFriendshipRange[1]
|
|
||||||
) {
|
|
||||||
const partyPokemon = globalScene.getPlayerParty();
|
const partyPokemon = globalScene.getPlayerParty();
|
||||||
const pokemonInRange = this.queryParty(partyPokemon);
|
const pokemonInRange = this.queryParty(partyPokemon);
|
||||||
if (pokemonInRange.length < this.minNumberOfPokemon) {
|
if (pokemonInRange.length < this.minNumberOfPokemon) {
|
||||||
@ -1071,7 +1054,7 @@ export class HealthRatioRequirement extends EncounterPokemonRequirement {
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
// Party Pokemon's health inside required health range
|
// Party Pokemon's health inside required health range
|
||||||
if (!isNullOrUndefined(this.requiredHealthRange) && this.requiredHealthRange[0] <= this.requiredHealthRange[1]) {
|
if (this.requiredHealthRange != null && this.requiredHealthRange[0] <= this.requiredHealthRange[1]) {
|
||||||
const partyPokemon = globalScene.getPlayerParty();
|
const partyPokemon = globalScene.getPlayerParty();
|
||||||
const pokemonInRange = this.queryParty(partyPokemon);
|
const pokemonInRange = this.queryParty(partyPokemon);
|
||||||
if (pokemonInRange.length < this.minNumberOfPokemon) {
|
if (pokemonInRange.length < this.minNumberOfPokemon) {
|
||||||
@ -1098,7 +1081,7 @@ export class HealthRatioRequirement extends EncounterPokemonRequirement {
|
|||||||
|
|
||||||
override getDialogueToken(pokemon?: PlayerPokemon): [string, string] {
|
override getDialogueToken(pokemon?: PlayerPokemon): [string, string] {
|
||||||
const hpRatio = pokemon?.getHpRatio();
|
const hpRatio = pokemon?.getHpRatio();
|
||||||
if (!isNullOrUndefined(hpRatio)) {
|
if (hpRatio != null) {
|
||||||
return ["healthRatio", Math.floor(hpRatio * 100).toString() + "%"];
|
return ["healthRatio", Math.floor(hpRatio * 100).toString() + "%"];
|
||||||
}
|
}
|
||||||
return ["healthRatio", ""];
|
return ["healthRatio", ""];
|
||||||
@ -1119,7 +1102,7 @@ export class WeightRequirement extends EncounterPokemonRequirement {
|
|||||||
|
|
||||||
override meetsRequirement(): boolean {
|
override meetsRequirement(): boolean {
|
||||||
// Party Pokemon's weight inside required weight range
|
// Party Pokemon's weight inside required weight range
|
||||||
if (!isNullOrUndefined(this.requiredWeightRange) && this.requiredWeightRange[0] <= this.requiredWeightRange[1]) {
|
if (this.requiredWeightRange != null && this.requiredWeightRange[0] <= this.requiredWeightRange[1]) {
|
||||||
const partyPokemon = globalScene.getPlayerParty();
|
const partyPokemon = globalScene.getPlayerParty();
|
||||||
const pokemonInRange = this.queryParty(partyPokemon);
|
const pokemonInRange = this.queryParty(partyPokemon);
|
||||||
if (pokemonInRange.length < this.minNumberOfPokemon) {
|
if (pokemonInRange.length < this.minNumberOfPokemon) {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT } from "#app/constants";
|
import { BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT } from "#app/constants";
|
||||||
import type { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
import type { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||||
import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
|
|
||||||
export class SeenEncounterData {
|
export class SeenEncounterData {
|
||||||
type: MysteryEncounterType;
|
type: MysteryEncounterType;
|
||||||
@ -28,7 +27,7 @@ export class MysteryEncounterSaveData {
|
|||||||
queuedEncounters: QueuedEncounter[] = [];
|
queuedEncounters: QueuedEncounter[] = [];
|
||||||
|
|
||||||
constructor(data?: MysteryEncounterSaveData) {
|
constructor(data?: MysteryEncounterSaveData) {
|
||||||
if (!isNullOrUndefined(data)) {
|
if (data != null) {
|
||||||
Object.assign(this, data);
|
Object.assign(this, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import {
|
|||||||
StatusEffectRequirement,
|
StatusEffectRequirement,
|
||||||
WaveRangeRequirement,
|
WaveRangeRequirement,
|
||||||
} from "#mystery-encounters/mystery-encounter-requirements";
|
} from "#mystery-encounters/mystery-encounter-requirements";
|
||||||
import { coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common";
|
import { coerceArray, randSeedInt } from "#utils/common";
|
||||||
import { capitalizeFirstLetter } from "#utils/strings";
|
import { capitalizeFirstLetter } from "#utils/strings";
|
||||||
|
|
||||||
export interface EncounterStartOfBattleEffect {
|
export interface EncounterStartOfBattleEffect {
|
||||||
@ -275,7 +275,7 @@ export class MysteryEncounter implements IMysteryEncounter {
|
|||||||
private seedOffset?: any;
|
private seedOffset?: any;
|
||||||
|
|
||||||
constructor(encounter: IMysteryEncounter | null) {
|
constructor(encounter: IMysteryEncounter | null) {
|
||||||
if (!isNullOrUndefined(encounter)) {
|
if (encounter != null) {
|
||||||
Object.assign(this, encounter);
|
Object.assign(this, encounter);
|
||||||
}
|
}
|
||||||
this.encounterTier = this.encounterTier ?? MysteryEncounterTier.COMMON;
|
this.encounterTier = this.encounterTier ?? MysteryEncounterTier.COMMON;
|
||||||
|
@ -3,7 +3,7 @@ import type { MoveId } from "#enums/move-id";
|
|||||||
import type { PlayerPokemon } from "#field/pokemon";
|
import type { PlayerPokemon } from "#field/pokemon";
|
||||||
import { PokemonMove } from "#moves/pokemon-move";
|
import { PokemonMove } from "#moves/pokemon-move";
|
||||||
import { EncounterPokemonRequirement } from "#mystery-encounters/mystery-encounter-requirements";
|
import { EncounterPokemonRequirement } from "#mystery-encounters/mystery-encounter-requirements";
|
||||||
import { coerceArray, isNullOrUndefined } from "#utils/common";
|
import { coerceArray } from "#utils/common";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@linkcode CanLearnMoveRequirement} options
|
* {@linkcode CanLearnMoveRequirement} options
|
||||||
@ -44,7 +44,7 @@ export class CanLearnMoveRequirement extends EncounterPokemonRequirement {
|
|||||||
.getPlayerParty()
|
.getPlayerParty()
|
||||||
.filter(pkm => (this.includeFainted ? pkm.isAllowedInChallenge() : pkm.isAllowedInBattle()));
|
.filter(pkm => (this.includeFainted ? pkm.isAllowedInChallenge() : pkm.isAllowedInBattle()));
|
||||||
|
|
||||||
if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) {
|
if (partyPokemon == null || this.requiredMoves?.length < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import type { TextStyle } from "#enums/text-style";
|
import type { TextStyle } from "#enums/text-style";
|
||||||
import { getTextWithColors } from "#ui/text";
|
import { getTextWithColors } from "#ui/text";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -11,7 +10,7 @@ import i18next from "i18next";
|
|||||||
* @param primaryStyle Can define a text style to be applied to the entire string. Must be defined for BBCodeText styles to be applied correctly
|
* @param primaryStyle Can define a text style to be applied to the entire string. Must be defined for BBCodeText styles to be applied correctly
|
||||||
*/
|
*/
|
||||||
export function getEncounterText(keyOrString?: string, primaryStyle?: TextStyle): string | null {
|
export function getEncounterText(keyOrString?: string, primaryStyle?: TextStyle): string | null {
|
||||||
if (isNullOrUndefined(keyOrString)) {
|
if (keyOrString == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ import type { HeldModifierConfig } from "#types/held-modifier-config";
|
|||||||
import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
||||||
import type { PartyOption, PokemonSelectFilter } from "#ui/party-ui-handler";
|
import type { PartyOption, PokemonSelectFilter } from "#ui/party-ui-handler";
|
||||||
import { PartyUiMode } from "#ui/party-ui-handler";
|
import { PartyUiMode } from "#ui/party-ui-handler";
|
||||||
import { coerceArray, isNullOrUndefined, randomString, randSeedInt, randSeedItem } from "#utils/common";
|
import { coerceArray, randomString, randSeedInt, randSeedItem } from "#utils/common";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
|
|||||||
const trainerType = partyConfig?.trainerType;
|
const trainerType = partyConfig?.trainerType;
|
||||||
const partyTrainerConfig = partyConfig?.trainerConfig;
|
const partyTrainerConfig = partyConfig?.trainerConfig;
|
||||||
let trainerConfig: TrainerConfig;
|
let trainerConfig: TrainerConfig;
|
||||||
if (!isNullOrUndefined(trainerType) || partyTrainerConfig) {
|
if (trainerType != null || partyTrainerConfig) {
|
||||||
globalScene.currentBattle.mysteryEncounter!.encounterMode = MysteryEncounterMode.TRAINER_BATTLE;
|
globalScene.currentBattle.mysteryEncounter!.encounterMode = MysteryEncounterMode.TRAINER_BATTLE;
|
||||||
if (globalScene.currentBattle.trainer) {
|
if (globalScene.currentBattle.trainer) {
|
||||||
globalScene.currentBattle.trainer.setVisible(false);
|
globalScene.currentBattle.trainer.setVisible(false);
|
||||||
@ -154,7 +154,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
|
|||||||
|
|
||||||
const doubleTrainer = trainerConfig.doubleOnly || (trainerConfig.hasDouble && !!partyConfig.doubleBattle);
|
const doubleTrainer = trainerConfig.doubleOnly || (trainerConfig.hasDouble && !!partyConfig.doubleBattle);
|
||||||
doubleBattle = doubleTrainer;
|
doubleBattle = doubleTrainer;
|
||||||
const trainerFemale = isNullOrUndefined(partyConfig.female) ? !!randSeedInt(2) : partyConfig.female;
|
const trainerFemale = partyConfig.female == null ? !!randSeedInt(2) : partyConfig.female;
|
||||||
const newTrainer = new Trainer(
|
const newTrainer = new Trainer(
|
||||||
trainerConfig.trainerType,
|
trainerConfig.trainerType,
|
||||||
doubleTrainer ? TrainerVariant.DOUBLE : trainerFemale ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
|
doubleTrainer ? TrainerVariant.DOUBLE : trainerFemale ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
|
||||||
@ -202,7 +202,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
|
|||||||
let dataSource: PokemonData | undefined;
|
let dataSource: PokemonData | undefined;
|
||||||
let isBoss = false;
|
let isBoss = false;
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
if ((!isNullOrUndefined(trainerType) || trainerConfig) && battle.trainer) {
|
if ((trainerType != null || trainerConfig) && battle.trainer) {
|
||||||
// Allows overriding a trainer's pokemon to use specific species/data
|
// Allows overriding a trainer's pokemon to use specific species/data
|
||||||
if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) {
|
if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) {
|
||||||
const config = partyConfig.pokemonConfigs[e];
|
const config = partyConfig.pokemonConfigs[e];
|
||||||
@ -258,7 +258,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
|
|||||||
enemyPokemon.resetSummonData();
|
enemyPokemon.resetSummonData();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((!loaded && isNullOrUndefined(partyConfig.countAsSeen)) || partyConfig.countAsSeen) {
|
if ((!loaded && partyConfig.countAsSeen == null) || partyConfig.countAsSeen) {
|
||||||
globalScene.gameData.setPokemonSeen(enemyPokemon, true, !!(trainerType || trainerConfig));
|
globalScene.gameData.setPokemonSeen(enemyPokemon, true, !!(trainerType || trainerConfig));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,7 +266,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
|
|||||||
const config = partyConfig.pokemonConfigs[e];
|
const config = partyConfig.pokemonConfigs[e];
|
||||||
|
|
||||||
// Set form
|
// Set form
|
||||||
if (!isNullOrUndefined(config.nickname)) {
|
if (config.nickname != null) {
|
||||||
enemyPokemon.nickname = btoa(unescape(encodeURIComponent(config.nickname)));
|
enemyPokemon.nickname = btoa(unescape(encodeURIComponent(config.nickname)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,22 +276,22 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set form
|
// Set form
|
||||||
if (!isNullOrUndefined(config.formIndex)) {
|
if (config.formIndex != null) {
|
||||||
enemyPokemon.formIndex = config.formIndex;
|
enemyPokemon.formIndex = config.formIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set shiny
|
// Set shiny
|
||||||
if (!isNullOrUndefined(config.shiny)) {
|
if (config.shiny != null) {
|
||||||
enemyPokemon.shiny = config.shiny;
|
enemyPokemon.shiny = config.shiny;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set Variant
|
// Set Variant
|
||||||
if (enemyPokemon.shiny && !isNullOrUndefined(config.variant)) {
|
if (enemyPokemon.shiny && config.variant != null) {
|
||||||
enemyPokemon.variant = config.variant;
|
enemyPokemon.variant = config.variant;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.)
|
// Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.)
|
||||||
if (!isNullOrUndefined(config.customPokemonData)) {
|
if (config.customPokemonData != null) {
|
||||||
enemyPokemon.customPokemonData = config.customPokemonData;
|
enemyPokemon.customPokemonData = config.customPokemonData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,7 +300,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
|
|||||||
let segments =
|
let segments =
|
||||||
config.bossSegments
|
config.bossSegments
|
||||||
?? globalScene.getEncounterBossSegments(globalScene.currentBattle.waveIndex, level, enemySpecies, true);
|
?? globalScene.getEncounterBossSegments(globalScene.currentBattle.waveIndex, level, enemySpecies, true);
|
||||||
if (!isNullOrUndefined(config.bossSegmentModifier)) {
|
if (config.bossSegmentModifier != null) {
|
||||||
segments += config.bossSegmentModifier;
|
segments += config.bossSegmentModifier;
|
||||||
}
|
}
|
||||||
enemyPokemon.setBoss(true, segments);
|
enemyPokemon.setBoss(true, segments);
|
||||||
@ -335,18 +335,18 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set ability
|
// Set ability
|
||||||
if (!isNullOrUndefined(config.abilityIndex)) {
|
if (config.abilityIndex != null) {
|
||||||
enemyPokemon.abilityIndex = config.abilityIndex;
|
enemyPokemon.abilityIndex = config.abilityIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set gender
|
// Set gender
|
||||||
if (!isNullOrUndefined(config.gender)) {
|
if (config.gender != null) {
|
||||||
enemyPokemon.gender = config.gender!;
|
enemyPokemon.gender = config.gender!;
|
||||||
enemyPokemon.summonData.gender = config.gender;
|
enemyPokemon.summonData.gender = config.gender;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set AI type
|
// Set AI type
|
||||||
if (!isNullOrUndefined(config.aiType)) {
|
if (config.aiType != null) {
|
||||||
enemyPokemon.aiType = config.aiType;
|
enemyPokemon.aiType = config.aiType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ import type { PartyOption } from "#ui/party-ui-handler";
|
|||||||
import { PartyUiMode } from "#ui/party-ui-handler";
|
import { PartyUiMode } from "#ui/party-ui-handler";
|
||||||
import { SummaryUiMode } from "#ui/summary-ui-handler";
|
import { SummaryUiMode } from "#ui/summary-ui-handler";
|
||||||
import { applyChallenges } from "#utils/challenge-utils";
|
import { applyChallenges } from "#utils/challenge-utils";
|
||||||
import { BooleanHolder, isNullOrUndefined, randSeedInt } from "#utils/common";
|
import { BooleanHolder, randSeedInt } from "#utils/common";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
@ -276,7 +276,7 @@ export function getRandomSpeciesByStarterCost(
|
|||||||
|
|
||||||
if (types && types.length > 0) {
|
if (types && types.length > 0) {
|
||||||
filteredSpecies = filteredSpecies.filter(
|
filteredSpecies = filteredSpecies.filter(
|
||||||
s => types.includes(s[0].type1) || (!isNullOrUndefined(s[0].type2) && types.includes(s[0].type2)),
|
s => types.includes(s[0].type1) || (s[0].type2 != null && types.includes(s[0].type2)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ import type { Variant, VariantSet } from "#sprites/variant";
|
|||||||
import { populateVariantColorCache, variantColorCache, variantData } from "#sprites/variant";
|
import { populateVariantColorCache, variantColorCache, variantData } from "#sprites/variant";
|
||||||
import type { Localizable } from "#types/locales";
|
import type { Localizable } from "#types/locales";
|
||||||
import type { StarterMoveset } from "#types/save-data";
|
import type { StarterMoveset } from "#types/save-data";
|
||||||
import { isNullOrUndefined, randSeedFloat, randSeedGauss, randSeedInt } from "#utils/common";
|
import { randSeedFloat, randSeedGauss, randSeedInt } from "#utils/common";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
import { toCamelCase, toPascalCase } from "#utils/strings";
|
import { toCamelCase, toPascalCase } from "#utils/strings";
|
||||||
import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities";
|
import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities";
|
||||||
@ -197,7 +197,7 @@ export abstract class PokemonSpeciesForm {
|
|||||||
* @returns The id of the ability
|
* @returns The id of the ability
|
||||||
*/
|
*/
|
||||||
getPassiveAbility(formIndex?: number): AbilityId {
|
getPassiveAbility(formIndex?: number): AbilityId {
|
||||||
if (isNullOrUndefined(formIndex)) {
|
if (formIndex == null) {
|
||||||
formIndex = this.formIndex;
|
formIndex = this.formIndex;
|
||||||
}
|
}
|
||||||
let starterSpeciesId = this.speciesId;
|
let starterSpeciesId = this.speciesId;
|
||||||
@ -551,7 +551,7 @@ export abstract class PokemonSpeciesForm {
|
|||||||
const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant, back);
|
const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant, back);
|
||||||
globalScene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant, back));
|
globalScene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant, back));
|
||||||
globalScene.load.audio(this.getCryKey(formIndex), `audio/${this.getCryKey(formIndex)}.m4a`);
|
globalScene.load.audio(this.getCryKey(formIndex), `audio/${this.getCryKey(formIndex)}.m4a`);
|
||||||
if (!isNullOrUndefined(variant)) {
|
if (variant != null) {
|
||||||
await this.loadVariantColors(spriteKey, female, variant, back, formIndex);
|
await this.loadVariantColors(spriteKey, female, variant, back, formIndex);
|
||||||
}
|
}
|
||||||
return new Promise<void>(resolve => {
|
return new Promise<void>(resolve => {
|
||||||
@ -579,7 +579,7 @@ export abstract class PokemonSpeciesForm {
|
|||||||
const spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant, back)
|
const spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant, back)
|
||||||
.replace("variant/", "")
|
.replace("variant/", "")
|
||||||
.replace(/_[1-3]$/, "");
|
.replace(/_[1-3]$/, "");
|
||||||
if (!isNullOrUndefined(variant)) {
|
if (variant != null) {
|
||||||
loadPokemonVariantAssets(spriteKey, spritePath, variant).then(() => resolve());
|
loadPokemonVariantAssets(spriteKey, spritePath, variant).then(() => resolve());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -791,7 +791,7 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable {
|
|||||||
* @returns A randomly rolled gender based on this Species' {@linkcode malePercent}.
|
* @returns A randomly rolled gender based on this Species' {@linkcode malePercent}.
|
||||||
*/
|
*/
|
||||||
generateGender(): Gender {
|
generateGender(): Gender {
|
||||||
if (isNullOrUndefined(this.malePercent)) {
|
if (this.malePercent == null) {
|
||||||
return Gender.GENDERLESS;
|
return Gender.GENDERLESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ import type { AttackMoveResult } from "#types/attack-move-result";
|
|||||||
import type { IllusionData } from "#types/illusion-data";
|
import type { IllusionData } from "#types/illusion-data";
|
||||||
import type { TurnMove } from "#types/turn-move";
|
import type { TurnMove } from "#types/turn-move";
|
||||||
import type { CoerceNullPropertiesToUndefined } from "#types/type-helpers";
|
import type { CoerceNullPropertiesToUndefined } from "#types/type-helpers";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
import { getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
import { getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -64,14 +63,14 @@ function deserializePokemonSpeciesForm(value: SerializedSpeciesForm | PokemonSpe
|
|||||||
// @ts-expect-error: We may be deserializing a PokemonSpeciesForm, but we catch later on
|
// @ts-expect-error: We may be deserializing a PokemonSpeciesForm, but we catch later on
|
||||||
let { id, formIdx } = value;
|
let { id, formIdx } = value;
|
||||||
|
|
||||||
if (isNullOrUndefined(id) || isNullOrUndefined(formIdx)) {
|
if (id == null || formIdx == null) {
|
||||||
// @ts-expect-error: Typescript doesn't know that in block, `value` must be a PokemonSpeciesForm
|
// @ts-expect-error: Typescript doesn't know that in block, `value` must be a PokemonSpeciesForm
|
||||||
id = value.speciesId;
|
id = value.speciesId;
|
||||||
// @ts-expect-error: Same as above (plus we are accessing a protected property)
|
// @ts-expect-error: Same as above (plus we are accessing a protected property)
|
||||||
formIdx = value._formIndex;
|
formIdx = value._formIndex;
|
||||||
}
|
}
|
||||||
// If for some reason either of these fields are null/undefined, we cannot reconstruct the species form
|
// If for some reason either of these fields are null/undefined, we cannot reconstruct the species form
|
||||||
if (isNullOrUndefined(id) || isNullOrUndefined(formIdx)) {
|
if (id == null || formIdx == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return getPokemonSpeciesForm(id, formIdx);
|
return getPokemonSpeciesForm(id, formIdx);
|
||||||
@ -151,13 +150,13 @@ export class PokemonSummonData {
|
|||||||
public moveHistory: TurnMove[] = [];
|
public moveHistory: TurnMove[] = [];
|
||||||
|
|
||||||
constructor(source?: PokemonSummonData | SerializedPokemonSummonData) {
|
constructor(source?: PokemonSummonData | SerializedPokemonSummonData) {
|
||||||
if (isNullOrUndefined(source)) {
|
if (source == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Rework this into an actual generic function for use elsewhere
|
// TODO: Rework this into an actual generic function for use elsewhere
|
||||||
for (const [key, value] of Object.entries(source)) {
|
for (const [key, value] of Object.entries(source)) {
|
||||||
if (isNullOrUndefined(value) && this.hasOwnProperty(key)) {
|
if (value == null && this.hasOwnProperty(key)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,7 +170,7 @@ export class PokemonSummonData {
|
|||||||
const illusionData = {
|
const illusionData = {
|
||||||
...value,
|
...value,
|
||||||
};
|
};
|
||||||
if (!isNullOrUndefined(illusionData.fusionSpecies)) {
|
if (illusionData.fusionSpecies != null) {
|
||||||
switch (typeof illusionData.fusionSpecies) {
|
switch (typeof illusionData.fusionSpecies) {
|
||||||
case "object":
|
case "object":
|
||||||
illusionData.fusionSpecies = allSpecies[illusionData.fusionSpecies.speciesId];
|
illusionData.fusionSpecies = allSpecies[illusionData.fusionSpecies.speciesId];
|
||||||
@ -224,18 +223,18 @@ export class PokemonSummonData {
|
|||||||
CoerceNullPropertiesToUndefined<PokemonSummonData>,
|
CoerceNullPropertiesToUndefined<PokemonSummonData>,
|
||||||
"speciesForm" | "fusionSpeciesForm" | "illusion"
|
"speciesForm" | "fusionSpeciesForm" | "illusion"
|
||||||
>),
|
>),
|
||||||
speciesForm: isNullOrUndefined(speciesForm)
|
speciesForm: speciesForm == null ? undefined : { id: speciesForm.speciesId, formIdx: speciesForm.formIndex },
|
||||||
? undefined
|
fusionSpeciesForm:
|
||||||
: { id: speciesForm.speciesId, formIdx: speciesForm.formIndex },
|
fusionSpeciesForm == null
|
||||||
fusionSpeciesForm: isNullOrUndefined(fusionSpeciesForm)
|
? undefined
|
||||||
? undefined
|
: { id: fusionSpeciesForm.speciesId, formIdx: fusionSpeciesForm.formIndex },
|
||||||
: { id: fusionSpeciesForm.speciesId, formIdx: fusionSpeciesForm.formIndex },
|
illusion:
|
||||||
illusion: isNullOrUndefined(illusion)
|
illusion == null
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
...(this.illusion as Omit<typeof illusion, "fusionSpecies">),
|
...(this.illusion as Omit<typeof illusion, "fusionSpecies">),
|
||||||
fusionSpecies: illusionSpeciesForm?.speciesId,
|
fusionSpecies: illusionSpeciesForm?.speciesId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// Replace `null` with `undefined`, as `undefined` never gets serialized
|
// Replace `null` with `undefined`, as `undefined` never gets serialized
|
||||||
for (const [key, value] of Object.entries(t)) {
|
for (const [key, value] of Object.entries(t)) {
|
||||||
@ -278,7 +277,7 @@ export class PokemonBattleData {
|
|||||||
public berriesEaten: BerryType[] = [];
|
public berriesEaten: BerryType[] = [];
|
||||||
|
|
||||||
constructor(source?: PokemonBattleData | Partial<PokemonBattleData>) {
|
constructor(source?: PokemonBattleData | Partial<PokemonBattleData>) {
|
||||||
if (!isNullOrUndefined(source)) {
|
if (source != null) {
|
||||||
this.hitCount = source.hitCount ?? 0;
|
this.hitCount = source.hitCount ?? 0;
|
||||||
this.hasEatenBerry = source.hasEatenBerry ?? false;
|
this.hasEatenBerry = source.hasEatenBerry ?? false;
|
||||||
this.berriesEaten = source.berriesEaten ?? [];
|
this.berriesEaten = source.berriesEaten ?? [];
|
||||||
|
@ -3,6 +3,8 @@ import { globalScene } from "#app/global-scene";
|
|||||||
import { pokemonEvolutions, pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
import { pokemonEvolutions, pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
||||||
import { signatureSpecies } from "#balance/signature-species";
|
import { signatureSpecies } from "#balance/signature-species";
|
||||||
import { tmSpecies } from "#balance/tms";
|
import { tmSpecies } from "#balance/tms";
|
||||||
|
// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment
|
||||||
|
import type { RARE_EGG_MOVE_LEVEL_REQUIREMENT } from "#data/balance/moveset-generation";
|
||||||
import { modifierTypes } from "#data/data-lists";
|
import { modifierTypes } from "#data/data-lists";
|
||||||
import { doubleBattleDialogue } from "#data/double-battle-dialogue";
|
import { doubleBattleDialogue } from "#data/double-battle-dialogue";
|
||||||
import { Gender } from "#data/gender";
|
import { Gender } from "#data/gender";
|
||||||
@ -41,7 +43,8 @@ import type {
|
|||||||
TrainerConfigs,
|
TrainerConfigs,
|
||||||
TrainerTierPools,
|
TrainerTierPools,
|
||||||
} from "#types/trainer-funcs";
|
} from "#types/trainer-funcs";
|
||||||
import { coerceArray, isNullOrUndefined, randSeedInt, randSeedIntRange, randSeedItem } from "#utils/common";
|
import type { Mutable } from "#types/type-helpers";
|
||||||
|
import { coerceArray, randSeedInt, randSeedIntRange, randSeedItem } from "#utils/common";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
import { toCamelCase, toTitleCase } from "#utils/strings";
|
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
@ -119,6 +122,15 @@ export class TrainerConfig {
|
|||||||
public hasVoucher = false;
|
public hasVoucher = false;
|
||||||
public trainerAI: TrainerAI;
|
public trainerAI: TrainerAI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this trainer's Pokémon are allowed to generate with egg moves
|
||||||
|
* @defaultValue `false`
|
||||||
|
*
|
||||||
|
* @see {@linkcode setEggMovesAllowed}
|
||||||
|
* @see {@linkcode RARE_EGG_MOVE_LEVEL_THRESHOLD}
|
||||||
|
*/
|
||||||
|
public readonly allowEggMoves: boolean = false;
|
||||||
|
|
||||||
public encounterMessages: string[] = [];
|
public encounterMessages: string[] = [];
|
||||||
public victoryMessages: string[] = [];
|
public victoryMessages: string[] = [];
|
||||||
public defeatMessages: string[] = [];
|
public defeatMessages: string[] = [];
|
||||||
@ -387,8 +399,27 @@ export class TrainerConfig {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
setBoss(): TrainerConfig {
|
/**
|
||||||
|
* Allow this trainer's Pokémon to have egg moves when generating their movesets.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* It is redundant to call this if {@linkcode setBoss} is also called on the configuration.
|
||||||
|
* @returns `this` for method chaining
|
||||||
|
* @see {@linkcode allowEggMoves}
|
||||||
|
*/
|
||||||
|
public setEggMovesAllowed(): this {
|
||||||
|
(this as Mutable<this>).allowEggMoves = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set this trainer as a boss trainer
|
||||||
|
* @returns `this` for method chaining
|
||||||
|
* @see {@linkcode isBoss}
|
||||||
|
*/
|
||||||
|
public setBoss(): TrainerConfig {
|
||||||
this.isBoss = true;
|
this.isBoss = true;
|
||||||
|
(this as Mutable<this>).allowEggMoves = true;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -474,7 +505,7 @@ export class TrainerConfig {
|
|||||||
.fill(null)
|
.fill(null)
|
||||||
.map((_, i) => i)
|
.map((_, i) => i)
|
||||||
.filter(i => shedinjaCanTera || party[i].species.speciesId !== SpeciesId.SHEDINJA); // Shedinja can only Tera on Bug specialty type (or no specialty type)
|
.filter(i => shedinjaCanTera || party[i].species.speciesId !== SpeciesId.SHEDINJA); // Shedinja can only Tera on Bug specialty type (or no specialty type)
|
||||||
const setPartySlot = !isNullOrUndefined(slot) ? Phaser.Math.Wrap(slot, 0, party.length) : -1; // If we have a tera slot defined, wrap it to party size.
|
const setPartySlot = slot != null ? Phaser.Math.Wrap(slot, 0, party.length) : -1; // If we have a tera slot defined, wrap it to party size.
|
||||||
for (let t = 0; t < Math.min(count(), party.length); t++) {
|
for (let t = 0; t < Math.min(count(), party.length); t++) {
|
||||||
const randomIndex =
|
const randomIndex =
|
||||||
partyMemberIndexes.indexOf(setPartySlot) > -1 ? setPartySlot : randSeedItem(partyMemberIndexes);
|
partyMemberIndexes.indexOf(setPartySlot) > -1 ? setPartySlot : randSeedItem(partyMemberIndexes);
|
||||||
@ -537,7 +568,7 @@ export class TrainerConfig {
|
|||||||
initI18n();
|
initI18n();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isNullOrUndefined(specialtyType)) {
|
if (specialtyType != null) {
|
||||||
this.setSpecialtyType(specialtyType);
|
this.setSpecialtyType(specialtyType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -612,7 +643,7 @@ export class TrainerConfig {
|
|||||||
signatureSpecies.forEach((speciesPool, s) => {
|
signatureSpecies.forEach((speciesPool, s) => {
|
||||||
this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(coerceArray(speciesPool)));
|
this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(coerceArray(speciesPool)));
|
||||||
});
|
});
|
||||||
if (!isNullOrUndefined(specialtyType)) {
|
if (specialtyType != null) {
|
||||||
this.setSpeciesFilter(p => p.isOfType(specialtyType));
|
this.setSpeciesFilter(p => p.isOfType(specialtyType));
|
||||||
this.setSpecialtyType(specialtyType);
|
this.setSpecialtyType(specialtyType);
|
||||||
}
|
}
|
||||||
@ -717,7 +748,7 @@ export class TrainerConfig {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Set species filter and specialty type if provided, otherwise filter by base total.
|
// Set species filter and specialty type if provided, otherwise filter by base total.
|
||||||
if (!isNullOrUndefined(specialtyType)) {
|
if (specialtyType != null) {
|
||||||
this.setSpeciesFilter(p => p.isOfType(specialtyType) && p.baseTotal >= ELITE_FOUR_MINIMUM_BST);
|
this.setSpeciesFilter(p => p.isOfType(specialtyType) && p.baseTotal >= ELITE_FOUR_MINIMUM_BST);
|
||||||
this.setSpecialtyType(specialtyType);
|
this.setSpecialtyType(specialtyType);
|
||||||
} else {
|
} else {
|
||||||
@ -895,7 +926,7 @@ export class TrainerConfig {
|
|||||||
* @returns `true` if `specialtyType` is defined and not {@link PokemonType.UNKNOWN}
|
* @returns `true` if `specialtyType` is defined and not {@link PokemonType.UNKNOWN}
|
||||||
*/
|
*/
|
||||||
hasSpecialtyType(): boolean {
|
hasSpecialtyType(): boolean {
|
||||||
return !isNullOrUndefined(this.specialtyType) && this.specialtyType !== PokemonType.UNKNOWN;
|
return this.specialtyType != null && this.specialtyType !== PokemonType.UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2942,7 +2973,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.SLOWBRO, SpeciesId.GALAR_SLOWBRO], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.SLOWBRO, SpeciesId.GALAR_SLOWBRO], TrainerSlot.TRAINER, true, p => {
|
||||||
// Tera Ice Slowbro/G-Slowbro
|
// Tera Ice Slowbro/G-Slowbro
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.ICE_BEAM)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.ICE_BEAM)) {
|
||||||
// Check if Ice Beam is in the moveset, if not, replace the third move with Ice Beam.
|
// Check if Ice Beam is in the moveset, if not, replace the third move with Ice Beam.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.ICE_BEAM);
|
p.moveset[2] = new PokemonMove(MoveId.ICE_BEAM);
|
||||||
}
|
}
|
||||||
@ -2967,7 +2998,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.STEELIX], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.STEELIX], TrainerSlot.TRAINER, true, p => {
|
||||||
// Tera Fighting Steelix
|
// Tera Fighting Steelix
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.BODY_PRESS)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.BODY_PRESS)) {
|
||||||
// Check if Body Press is in the moveset, if not, replace the third move with Body Press.
|
// Check if Body Press is in the moveset, if not, replace the third move with Body Press.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.BODY_PRESS);
|
p.moveset[2] = new PokemonMove(MoveId.BODY_PRESS);
|
||||||
}
|
}
|
||||||
@ -2992,7 +3023,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.ARBOK, SpeciesId.WEEZING], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.ARBOK, SpeciesId.WEEZING], TrainerSlot.TRAINER, true, p => {
|
||||||
// Tera Ghost Arbok/Weezing
|
// Tera Ghost Arbok/Weezing
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) {
|
||||||
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
||||||
}
|
}
|
||||||
@ -3018,7 +3049,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.GYARADOS, SpeciesId.AERODACTYL], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.GYARADOS, SpeciesId.AERODACTYL], TrainerSlot.TRAINER, true, p => {
|
||||||
// Tera Dragon Gyarados/Aerodactyl
|
// Tera Dragon Gyarados/Aerodactyl
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) {
|
||||||
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
||||||
}
|
}
|
||||||
@ -3079,7 +3110,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.GENGAR], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.GENGAR], TrainerSlot.TRAINER, true, p => {
|
||||||
// Tera Dark Gengar
|
// Tera Dark Gengar
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.DARK_PULSE)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.DARK_PULSE)) {
|
||||||
// Check if Dark Pulse is in the moveset, if not, replace the third move with Dark Pulse.
|
// Check if Dark Pulse is in the moveset, if not, replace the third move with Dark Pulse.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.DARK_PULSE);
|
p.moveset[2] = new PokemonMove(MoveId.DARK_PULSE);
|
||||||
}
|
}
|
||||||
@ -3163,7 +3194,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.DHELMISE], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.DHELMISE], TrainerSlot.TRAINER, true, p => {
|
||||||
// Tera Dragon Dhelmise
|
// Tera Dragon Dhelmise
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) {
|
||||||
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
||||||
}
|
}
|
||||||
@ -3193,7 +3224,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
p.setBoss(true, 2);
|
p.setBoss(true, 2);
|
||||||
p.abilityIndex = 1; // Sniper
|
p.abilityIndex = 1; // Sniper
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.X_SCISSOR)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.X_SCISSOR)) {
|
||||||
// Check if X-Scissor is in the moveset, if not, replace the third move with X-Scissor.
|
// Check if X-Scissor is in the moveset, if not, replace the third move with X-Scissor.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.X_SCISSOR);
|
p.moveset[2] = new PokemonMove(MoveId.X_SCISSOR);
|
||||||
}
|
}
|
||||||
@ -3232,7 +3263,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.STEELIX, SpeciesId.LOPUNNY], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.STEELIX, SpeciesId.LOPUNNY], TrainerSlot.TRAINER, true, p => {
|
||||||
// Tera Fire Steelix/Lopunny
|
// Tera Fire Steelix/Lopunny
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) {
|
||||||
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
||||||
}
|
}
|
||||||
@ -3375,7 +3406,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.CERULEDGE], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.CERULEDGE], TrainerSlot.TRAINER, true, p => {
|
||||||
// Tera Steel Ceruledge
|
// Tera Steel Ceruledge
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.IRON_HEAD)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.IRON_HEAD)) {
|
||||||
// Check if Iron Head is in the moveset, if not, replace the third move with Iron Head.
|
// Check if Iron Head is in the moveset, if not, replace the third move with Iron Head.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.IRON_HEAD);
|
p.moveset[2] = new PokemonMove(MoveId.IRON_HEAD);
|
||||||
}
|
}
|
||||||
@ -3413,7 +3444,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.INCINEROAR], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.INCINEROAR], TrainerSlot.TRAINER, true, p => {
|
||||||
// Tera Fighting Incineroar
|
// Tera Fighting Incineroar
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.CROSS_CHOP)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.CROSS_CHOP)) {
|
||||||
// Check if Cross Chop is in the moveset, if not, replace the third move with Cross Chop.
|
// Check if Cross Chop is in the moveset, if not, replace the third move with Cross Chop.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.CROSS_CHOP);
|
p.moveset[2] = new PokemonMove(MoveId.CROSS_CHOP);
|
||||||
}
|
}
|
||||||
@ -3486,7 +3517,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.DECIDUEYE], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.DECIDUEYE], TrainerSlot.TRAINER, true, p => {
|
||||||
// Tera Flying Decidueye
|
// Tera Flying Decidueye
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.BRAVE_BIRD)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.BRAVE_BIRD)) {
|
||||||
// Check if Brave Bird is in the moveset, if not, replace the third move with Brave Bird.
|
// Check if Brave Bird is in the moveset, if not, replace the third move with Brave Bird.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.BRAVE_BIRD);
|
p.moveset[2] = new PokemonMove(MoveId.BRAVE_BIRD);
|
||||||
}
|
}
|
||||||
@ -3511,7 +3542,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.TOXICROAK], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.TOXICROAK], TrainerSlot.TRAINER, true, p => {
|
||||||
// Tera Dark Toxicroak
|
// Tera Dark Toxicroak
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.SUCKER_PUNCH)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.SUCKER_PUNCH)) {
|
||||||
// Check if Sucker Punch is in the moveset, if not, replace the third move with Sucker Punch.
|
// Check if Sucker Punch is in the moveset, if not, replace the third move with Sucker Punch.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.SUCKER_PUNCH);
|
p.moveset[2] = new PokemonMove(MoveId.SUCKER_PUNCH);
|
||||||
}
|
}
|
||||||
@ -3536,7 +3567,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.EISCUE], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.EISCUE], TrainerSlot.TRAINER, true, p => {
|
||||||
// Tera Water Eiscue
|
// Tera Water Eiscue
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.LIQUIDATION)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.LIQUIDATION)) {
|
||||||
// Check if Liquidation is in the moveset, if not, replace the third move with Liquidation.
|
// Check if Liquidation is in the moveset, if not, replace the third move with Liquidation.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.LIQUIDATION);
|
p.moveset[2] = new PokemonMove(MoveId.LIQUIDATION);
|
||||||
}
|
}
|
||||||
@ -3598,7 +3629,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
// Tera Dragon Torkoal
|
// Tera Dragon Torkoal
|
||||||
p.abilityIndex = 1; // Drought
|
p.abilityIndex = 1; // Drought
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) {
|
||||||
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
||||||
}
|
}
|
||||||
@ -3695,7 +3726,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.EXEGGUTOR], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.EXEGGUTOR], TrainerSlot.TRAINER, true, p => {
|
||||||
// Tera Fire Exeggutor
|
// Tera Fire Exeggutor
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) {
|
||||||
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
||||||
}
|
}
|
||||||
@ -3705,7 +3736,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
3,
|
3,
|
||||||
getRandomPartyMemberFunc([SpeciesId.TALONFLAME], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.TALONFLAME], TrainerSlot.TRAINER, true, p => {
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.SUNNY_DAY)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.SUNNY_DAY)) {
|
||||||
// Check if Sunny Day is in the moveset, if not, replace the third move with Sunny Day.
|
// Check if Sunny Day is in the moveset, if not, replace the third move with Sunny Day.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.SUNNY_DAY);
|
p.moveset[2] = new PokemonMove(MoveId.SUNNY_DAY);
|
||||||
}
|
}
|
||||||
@ -3728,7 +3759,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.REUNICLUS], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.REUNICLUS], TrainerSlot.TRAINER, true, p => {
|
||||||
// Tera Steel Reuniclus
|
// Tera Steel Reuniclus
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.FLASH_CANNON)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.FLASH_CANNON)) {
|
||||||
// Check if Flash Cannon is in the moveset, if not, replace the third move with Flash Cannon.
|
// Check if Flash Cannon is in the moveset, if not, replace the third move with Flash Cannon.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.FLASH_CANNON);
|
p.moveset[2] = new PokemonMove(MoveId.FLASH_CANNON);
|
||||||
}
|
}
|
||||||
@ -3756,7 +3787,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
// Tera Fairy Excadrill
|
// Tera Fairy Excadrill
|
||||||
p.setBoss(true, 2);
|
p.setBoss(true, 2);
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) {
|
||||||
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
||||||
}
|
}
|
||||||
@ -3771,7 +3802,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.SCEPTILE], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.SCEPTILE], TrainerSlot.TRAINER, true, p => {
|
||||||
// Tera Dragon Sceptile
|
// Tera Dragon Sceptile
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.DUAL_CHOP)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.DUAL_CHOP)) {
|
||||||
// Check if Dual Chop is in the moveset, if not, replace the third move with Dual Chop.
|
// Check if Dual Chop is in the moveset, if not, replace the third move with Dual Chop.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.DUAL_CHOP);
|
p.moveset[2] = new PokemonMove(MoveId.DUAL_CHOP);
|
||||||
}
|
}
|
||||||
@ -3841,7 +3872,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
p.formIndex = 1; // Partner Pikachu
|
p.formIndex = 1; // Partner Pikachu
|
||||||
p.gender = Gender.MALE;
|
p.gender = Gender.MALE;
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.VOLT_TACKLE)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.VOLT_TACKLE)) {
|
||||||
// Check if Volt Tackle is in the moveset, if not, replace the first move with Volt Tackle.
|
// Check if Volt Tackle is in the moveset, if not, replace the first move with Volt Tackle.
|
||||||
p.moveset[0] = new PokemonMove(MoveId.VOLT_TACKLE);
|
p.moveset[0] = new PokemonMove(MoveId.VOLT_TACKLE);
|
||||||
}
|
}
|
||||||
@ -4072,7 +4103,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.KELDEO], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.KELDEO], TrainerSlot.TRAINER, true, p => {
|
||||||
p.pokeball = PokeballType.ROGUE_BALL;
|
p.pokeball = PokeballType.ROGUE_BALL;
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.SECRET_SWORD)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.SECRET_SWORD)) {
|
||||||
// Check if Secret Sword is in the moveset, if not, replace the third move with Secret Sword.
|
// Check if Secret Sword is in the moveset, if not, replace the third move with Secret Sword.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.SECRET_SWORD);
|
p.moveset[2] = new PokemonMove(MoveId.SECRET_SWORD);
|
||||||
}
|
}
|
||||||
@ -4401,7 +4432,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
5,
|
5,
|
||||||
getRandomPartyMemberFunc([SpeciesId.KINGAMBIT], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.KINGAMBIT], TrainerSlot.TRAINER, true, p => {
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) {
|
||||||
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
||||||
}
|
}
|
||||||
@ -4480,7 +4511,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
4,
|
4,
|
||||||
getRandomPartyMemberFunc([SpeciesId.TERAPAGOS], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.TERAPAGOS], TrainerSlot.TRAINER, true, p => {
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_STARSTORM)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_STARSTORM)) {
|
||||||
// Check if Tera Starstorm is in the moveset, if not, replace the first move with Tera Starstorm.
|
// Check if Tera Starstorm is in the moveset, if not, replace the first move with Tera Starstorm.
|
||||||
p.moveset[0] = new PokemonMove(MoveId.TERA_STARSTORM);
|
p.moveset[0] = new PokemonMove(MoveId.TERA_STARSTORM);
|
||||||
}
|
}
|
||||||
@ -4494,7 +4525,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
p.setBoss(true, 2);
|
p.setBoss(true, 2);
|
||||||
p.teraType = PokemonType.FIGHTING;
|
p.teraType = PokemonType.FIGHTING;
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) {
|
||||||
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
// Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST);
|
||||||
}
|
}
|
||||||
@ -5054,7 +5085,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
2,
|
2,
|
||||||
getRandomPartyMemberFunc([SpeciesId.HONCHKROW], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.HONCHKROW], TrainerSlot.TRAINER, true, p => {
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.SUCKER_PUNCH)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.SUCKER_PUNCH)) {
|
||||||
// Check if Sucker Punch is in the moveset, if not, replace the third move with Sucker Punch.
|
// Check if Sucker Punch is in the moveset, if not, replace the third move with Sucker Punch.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.SUCKER_PUNCH);
|
p.moveset[2] = new PokemonMove(MoveId.SUCKER_PUNCH);
|
||||||
}
|
}
|
||||||
@ -5517,7 +5548,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
p.formIndex = randSeedInt(18); // Random Silvally Form
|
p.formIndex = randSeedInt(18); // Random Silvally Form
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
p.pokeball = PokeballType.ROGUE_BALL;
|
p.pokeball = PokeballType.ROGUE_BALL;
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.MULTI_ATTACK)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.MULTI_ATTACK)) {
|
||||||
// Check if Multi Attack is in the moveset, if not, replace the first move with Multi Attack.
|
// Check if Multi Attack is in the moveset, if not, replace the first move with Multi Attack.
|
||||||
p.moveset[0] = new PokemonMove(MoveId.MULTI_ATTACK);
|
p.moveset[0] = new PokemonMove(MoveId.MULTI_ATTACK);
|
||||||
}
|
}
|
||||||
@ -5590,7 +5621,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.GOLISOPOD], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.GOLISOPOD], TrainerSlot.TRAINER, true, p => {
|
||||||
p.setBoss(true, 2);
|
p.setBoss(true, 2);
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.FIRST_IMPRESSION)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.FIRST_IMPRESSION)) {
|
||||||
// Check if First Impression is in the moveset, if not, replace the third move with First Impression.
|
// Check if First Impression is in the moveset, if not, replace the third move with First Impression.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.FIRST_IMPRESSION);
|
p.moveset[2] = new PokemonMove(MoveId.FIRST_IMPRESSION);
|
||||||
p.gender = Gender.MALE;
|
p.gender = Gender.MALE;
|
||||||
@ -5607,7 +5638,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
getRandomPartyMemberFunc([SpeciesId.GOLISOPOD], TrainerSlot.TRAINER, true, p => {
|
getRandomPartyMemberFunc([SpeciesId.GOLISOPOD], TrainerSlot.TRAINER, true, p => {
|
||||||
p.setBoss(true, 2);
|
p.setBoss(true, 2);
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.FIRST_IMPRESSION)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.FIRST_IMPRESSION)) {
|
||||||
// Check if First Impression is in the moveset, if not, replace the third move with First Impression.
|
// Check if First Impression is in the moveset, if not, replace the third move with First Impression.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.FIRST_IMPRESSION);
|
p.moveset[2] = new PokemonMove(MoveId.FIRST_IMPRESSION);
|
||||||
p.abilityIndex = 2; // Anticipation
|
p.abilityIndex = 2; // Anticipation
|
||||||
@ -5643,7 +5674,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
p.pokeball = PokeballType.ROGUE_BALL;
|
p.pokeball = PokeballType.ROGUE_BALL;
|
||||||
p.formIndex = randSeedInt(4, 1); // Shock, Burn, Chill, or Douse Drive
|
p.formIndex = randSeedInt(4, 1); // Shock, Burn, Chill, or Douse Drive
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TECHNO_BLAST)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.TECHNO_BLAST)) {
|
||||||
// Check if Techno Blast is in the moveset, if not, replace the third move with Techno Blast.
|
// Check if Techno Blast is in the moveset, if not, replace the third move with Techno Blast.
|
||||||
p.moveset[2] = new PokemonMove(MoveId.TECHNO_BLAST);
|
p.moveset[2] = new PokemonMove(MoveId.TECHNO_BLAST);
|
||||||
}
|
}
|
||||||
@ -5778,7 +5809,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
p.setBoss(true, 2);
|
p.setBoss(true, 2);
|
||||||
p.abilityIndex = 2; // Pixilate
|
p.abilityIndex = 2; // Pixilate
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.HYPER_VOICE)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.HYPER_VOICE)) {
|
||||||
// Check if Hyper Voice is in the moveset, if not, replace the second move with Hyper Voice.
|
// Check if Hyper Voice is in the moveset, if not, replace the second move with Hyper Voice.
|
||||||
p.moveset[1] = new PokemonMove(MoveId.HYPER_VOICE);
|
p.moveset[1] = new PokemonMove(MoveId.HYPER_VOICE);
|
||||||
p.gender = Gender.FEMALE;
|
p.gender = Gender.FEMALE;
|
||||||
@ -5807,7 +5838,7 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
p.setBoss(true, 2);
|
p.setBoss(true, 2);
|
||||||
p.abilityIndex = 2; // Pixilate
|
p.abilityIndex = 2; // Pixilate
|
||||||
p.generateAndPopulateMoveset();
|
p.generateAndPopulateMoveset();
|
||||||
if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.HYPER_VOICE)) {
|
if (!p.moveset.some(move => move != null && move.moveId === MoveId.HYPER_VOICE)) {
|
||||||
// Check if Hyper Voice is in the moveset, if not, replace the second move with Hyper Voice.
|
// Check if Hyper Voice is in the moveset, if not, replace the second move with Hyper Voice.
|
||||||
p.moveset[1] = new PokemonMove(MoveId.HYPER_VOICE);
|
p.moveset[1] = new PokemonMove(MoveId.HYPER_VOICE);
|
||||||
p.gender = Gender.FEMALE;
|
p.gender = Gender.FEMALE;
|
||||||
|
@ -36,7 +36,7 @@ import type { Pokemon } from "#field/pokemon";
|
|||||||
import { FieldEffectModifier } from "#modifiers/modifier";
|
import { FieldEffectModifier } from "#modifiers/modifier";
|
||||||
import type { Move } from "#moves/move";
|
import type { Move } from "#moves/move";
|
||||||
import type { AbstractConstructor } from "#types/type-helpers";
|
import type { AbstractConstructor } from "#types/type-helpers";
|
||||||
import { type Constructor, isNullOrUndefined, NumberHolder, randSeedInt } from "#utils/common";
|
import { type Constructor, NumberHolder, randSeedInt } from "#utils/common";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
|
|
||||||
export class Arena {
|
export class Arena {
|
||||||
@ -339,7 +339,7 @@ export class Arena {
|
|||||||
|
|
||||||
const weatherDuration = new NumberHolder(0);
|
const weatherDuration = new NumberHolder(0);
|
||||||
|
|
||||||
if (!isNullOrUndefined(user)) {
|
if (user != null) {
|
||||||
weatherDuration.value = 5;
|
weatherDuration.value = 5;
|
||||||
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration);
|
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration);
|
||||||
}
|
}
|
||||||
@ -420,7 +420,7 @@ export class Arena {
|
|||||||
|
|
||||||
const terrainDuration = new NumberHolder(0);
|
const terrainDuration = new NumberHolder(0);
|
||||||
|
|
||||||
if (!isNullOrUndefined(user)) {
|
if (user != null) {
|
||||||
terrainDuration.value = 5;
|
terrainDuration.value = 5;
|
||||||
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration);
|
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import { getSpriteKeysFromSpecies } from "#mystery-encounters/encounter-pokemon-
|
|||||||
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
|
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
|
||||||
import { loadPokemonVariantAssets } from "#sprites/pokemon-sprite";
|
import { loadPokemonVariantAssets } from "#sprites/pokemon-sprite";
|
||||||
import type { Variant } from "#sprites/variant";
|
import type { Variant } from "#sprites/variant";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
import type { GameObjects } from "phaser";
|
import type { GameObjects } from "phaser";
|
||||||
|
|
||||||
type PlayAnimationConfig = Phaser.Types.Animations.PlayAnimationConfig;
|
type PlayAnimationConfig = Phaser.Types.Animations.PlayAnimationConfig;
|
||||||
@ -98,7 +97,7 @@ export class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container {
|
|||||||
...config,
|
...config,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isNullOrUndefined(result.species)) {
|
if (result.species != null) {
|
||||||
const keys = getSpriteKeysFromSpecies(result.species, undefined, undefined, result.isShiny, result.variant);
|
const keys = getSpriteKeysFromSpecies(result.species, undefined, undefined, result.isShiny, result.variant);
|
||||||
result.spriteKey = keys.spriteKey;
|
result.spriteKey = keys.spriteKey;
|
||||||
result.fileRoot = keys.fileRoot;
|
result.fileRoot = keys.fileRoot;
|
||||||
@ -205,12 +204,12 @@ export class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container {
|
|||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isNullOrUndefined(pokemonShinySparkle)) {
|
if (pokemonShinySparkle != null) {
|
||||||
// Offset the sparkle to match the Pokemon's position
|
// Offset the sparkle to match the Pokemon's position
|
||||||
pokemonShinySparkle.setPosition(sprite.x, sprite.y);
|
pokemonShinySparkle.setPosition(sprite.x, sprite.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isNullOrUndefined(alpha)) {
|
if (alpha != null) {
|
||||||
sprite.setAlpha(alpha);
|
sprite.setAlpha(alpha);
|
||||||
tintSprite.setAlpha(alpha);
|
tintSprite.setAlpha(alpha);
|
||||||
}
|
}
|
||||||
@ -234,7 +233,7 @@ export class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container {
|
|||||||
this.spriteConfigs.forEach(config => {
|
this.spriteConfigs.forEach(config => {
|
||||||
if (config.isPokemon) {
|
if (config.isPokemon) {
|
||||||
globalScene.loadPokemonAtlas(config.spriteKey, config.fileRoot);
|
globalScene.loadPokemonAtlas(config.spriteKey, config.fileRoot);
|
||||||
if (config.isShiny && !isNullOrUndefined(config.variant)) {
|
if (config.isShiny && config.variant != null) {
|
||||||
shinyPromises.push(loadPokemonVariantAssets(config.spriteKey, config.fileRoot, config.variant));
|
shinyPromises.push(loadPokemonVariantAssets(config.spriteKey, config.fileRoot, config.variant));
|
||||||
}
|
}
|
||||||
} else if (config.isItem) {
|
} else if (config.isItem) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability";
|
import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability";
|
||||||
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
|
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
|
||||||
|
import { generateMoveset } from "#app/ai/ai-moveset-gen";
|
||||||
import type { AnySound, BattleScene } from "#app/battle-scene";
|
import type { AnySound, BattleScene } from "#app/battle-scene";
|
||||||
import { PLAYER_PARTY_MAX_SIZE, RARE_CANDY_FRIENDSHIP_CAP } from "#app/constants";
|
import { PLAYER_PARTY_MAX_SIZE, RARE_CANDY_FRIENDSHIP_CAP } from "#app/constants";
|
||||||
import { timedEventManager } from "#app/global-event-manager";
|
import { timedEventManager } from "#app/global-event-manager";
|
||||||
@ -18,7 +19,7 @@ import type { LevelMoves } from "#balance/pokemon-level-moves";
|
|||||||
import { EVOLVE_MOVE, RELEARN_MOVE } from "#balance/pokemon-level-moves";
|
import { EVOLVE_MOVE, RELEARN_MOVE } from "#balance/pokemon-level-moves";
|
||||||
import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE, SHINY_EPIC_CHANCE, SHINY_VARIANT_CHANCE } from "#balance/rates";
|
import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE, SHINY_EPIC_CHANCE, SHINY_VARIANT_CHANCE } from "#balance/rates";
|
||||||
import { getStarterValueFriendshipCap, speciesStarterCosts } from "#balance/starters";
|
import { getStarterValueFriendshipCap, speciesStarterCosts } from "#balance/starters";
|
||||||
import { reverseCompatibleTms, tmPoolTiers, tmSpecies } from "#balance/tms";
|
import { reverseCompatibleTms, tmSpecies } from "#balance/tms";
|
||||||
import type { SuppressAbilitiesTag } from "#data/arena-tag";
|
import type { SuppressAbilitiesTag } from "#data/arena-tag";
|
||||||
import { NoCritTag, WeakenMoveScreenTag } from "#data/arena-tag";
|
import { NoCritTag, WeakenMoveScreenTag } from "#data/arena-tag";
|
||||||
import {
|
import {
|
||||||
@ -81,7 +82,6 @@ import { DexAttr } from "#enums/dex-attr";
|
|||||||
import { FieldPosition } from "#enums/field-position";
|
import { FieldPosition } from "#enums/field-position";
|
||||||
import { HitResult } from "#enums/hit-result";
|
import { HitResult } from "#enums/hit-result";
|
||||||
import { LearnMoveSituation } from "#enums/learn-move-situation";
|
import { LearnMoveSituation } from "#enums/learn-move-situation";
|
||||||
import { ModifierTier } from "#enums/modifier-tier";
|
|
||||||
import { MoveCategory } from "#enums/move-category";
|
import { MoveCategory } from "#enums/move-category";
|
||||||
import { MoveFlags } from "#enums/move-flags";
|
import { MoveFlags } from "#enums/move-flags";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
@ -159,7 +159,6 @@ import {
|
|||||||
fixedInt,
|
fixedInt,
|
||||||
getIvsFromId,
|
getIvsFromId,
|
||||||
isBetween,
|
isBetween,
|
||||||
isNullOrUndefined,
|
|
||||||
NumberHolder,
|
NumberHolder,
|
||||||
randSeedFloat,
|
randSeedFloat,
|
||||||
randSeedInt,
|
randSeedInt,
|
||||||
@ -886,7 +885,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
return this.fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error);
|
return this.fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error);
|
||||||
})
|
})
|
||||||
.then(c => {
|
.then(c => {
|
||||||
if (!isNullOrUndefined(c)) {
|
if (c != null) {
|
||||||
variantColorCache[cacheKey] = c;
|
variantColorCache[cacheKey] = c;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1475,7 +1474,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ally = this.getAlly();
|
const ally = this.getAlly();
|
||||||
if (!isNullOrUndefined(ally)) {
|
if (ally != null) {
|
||||||
applyAbAttrs("AllyStatMultiplierAbAttr", {
|
applyAbAttrs("AllyStatMultiplierAbAttr", {
|
||||||
pokemon: ally,
|
pokemon: ally,
|
||||||
stat,
|
stat,
|
||||||
@ -1658,7 +1657,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
if (useIllusion && this.summonData.illusion) {
|
if (useIllusion && this.summonData.illusion) {
|
||||||
return this.summonData.illusion.gender;
|
return this.summonData.illusion.gender;
|
||||||
}
|
}
|
||||||
if (!ignoreOverride && !isNullOrUndefined(this.summonData.gender)) {
|
if (!ignoreOverride && this.summonData.gender != null) {
|
||||||
return this.summonData.gender;
|
return this.summonData.gender;
|
||||||
}
|
}
|
||||||
return this.gender;
|
return this.gender;
|
||||||
@ -1674,7 +1673,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
if (useIllusion && this.summonData.illusion?.fusionGender) {
|
if (useIllusion && this.summonData.illusion?.fusionGender) {
|
||||||
return this.summonData.illusion.fusionGender;
|
return this.summonData.illusion.fusionGender;
|
||||||
}
|
}
|
||||||
if (!ignoreOverride && !isNullOrUndefined(this.summonData.fusionGender)) {
|
if (!ignoreOverride && this.summonData.fusionGender != null) {
|
||||||
return this.summonData.fusionGender;
|
return this.summonData.fusionGender;
|
||||||
}
|
}
|
||||||
return this.fusionGender;
|
return this.fusionGender;
|
||||||
@ -1793,7 +1792,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* @returns Whether this Pokemon has this species as either its base or fusion counterpart.
|
* @returns Whether this Pokemon has this species as either its base or fusion counterpart.
|
||||||
*/
|
*/
|
||||||
hasSpecies(species: SpeciesId, formKey?: string): boolean {
|
hasSpecies(species: SpeciesId, formKey?: string): boolean {
|
||||||
if (isNullOrUndefined(formKey)) {
|
if (formKey == null) {
|
||||||
return this.species.speciesId === species || this.fusionSpecies?.speciesId === species;
|
return this.species.speciesId === species || this.fusionSpecies?.speciesId === species;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1941,7 +1940,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
secondType = fusionType1;
|
secondType = fusionType1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (secondType === PokemonType.UNKNOWN && isNullOrUndefined(fusionType2)) {
|
if (secondType === PokemonType.UNKNOWN && fusionType2 == null) {
|
||||||
// If second pokemon was monotype and shared its primary type
|
// If second pokemon was monotype and shared its primary type
|
||||||
secondType =
|
secondType =
|
||||||
customTypes
|
customTypes
|
||||||
@ -2024,12 +2023,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
return allAbilities[Overrides.ENEMY_ABILITY_OVERRIDE];
|
return allAbilities[Overrides.ENEMY_ABILITY_OVERRIDE];
|
||||||
}
|
}
|
||||||
if (this.isFusion()) {
|
if (this.isFusion()) {
|
||||||
if (!isNullOrUndefined(this.fusionCustomPokemonData?.ability) && this.fusionCustomPokemonData.ability !== -1) {
|
if (this.fusionCustomPokemonData?.ability != null && this.fusionCustomPokemonData.ability !== -1) {
|
||||||
return allAbilities[this.fusionCustomPokemonData.ability];
|
return allAbilities[this.fusionCustomPokemonData.ability];
|
||||||
}
|
}
|
||||||
return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)];
|
return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)];
|
||||||
}
|
}
|
||||||
if (!isNullOrUndefined(this.customPokemonData.ability) && this.customPokemonData.ability !== -1) {
|
if (this.customPokemonData.ability != null && this.customPokemonData.ability !== -1) {
|
||||||
return allAbilities[this.customPokemonData.ability];
|
return allAbilities[this.customPokemonData.ability];
|
||||||
}
|
}
|
||||||
let abilityId = this.getSpeciesForm(ignoreOverride).getAbility(this.abilityIndex);
|
let abilityId = this.getSpeciesForm(ignoreOverride).getAbility(this.abilityIndex);
|
||||||
@ -2053,7 +2052,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
if (Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE && this.isEnemy()) {
|
if (Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE && this.isEnemy()) {
|
||||||
return allAbilities[Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE];
|
return allAbilities[Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE];
|
||||||
}
|
}
|
||||||
if (!isNullOrUndefined(this.customPokemonData.passive) && this.customPokemonData.passive !== -1) {
|
if (this.customPokemonData.passive != null && this.customPokemonData.passive !== -1) {
|
||||||
return allAbilities[this.customPokemonData.passive];
|
return allAbilities[this.customPokemonData.passive];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2237,7 +2236,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
public getWeight(): number {
|
public getWeight(): number {
|
||||||
const autotomizedTag = this.getTag(AutotomizedTag);
|
const autotomizedTag = this.getTag(AutotomizedTag);
|
||||||
let weightRemoved = 0;
|
let weightRemoved = 0;
|
||||||
if (!isNullOrUndefined(autotomizedTag)) {
|
if (autotomizedTag != null) {
|
||||||
weightRemoved = 100 * autotomizedTag.autotomizeCount;
|
weightRemoved = 100 * autotomizedTag.autotomizeCount;
|
||||||
}
|
}
|
||||||
const minWeight = 0.1;
|
const minWeight = 0.1;
|
||||||
@ -2384,7 +2383,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
cancelled?: BooleanHolder,
|
cancelled?: BooleanHolder,
|
||||||
useIllusion = false,
|
useIllusion = false,
|
||||||
): TypeDamageMultiplier {
|
): TypeDamageMultiplier {
|
||||||
if (!isNullOrUndefined(this.turnData?.moveEffectiveness)) {
|
if (this.turnData?.moveEffectiveness != null) {
|
||||||
return this.turnData?.moveEffectiveness;
|
return this.turnData?.moveEffectiveness;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2636,7 +2635,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
e => new FusionSpeciesFormEvolution(this.species.speciesId, e),
|
e => new FusionSpeciesFormEvolution(this.species.speciesId, e),
|
||||||
);
|
);
|
||||||
for (const fe of fusionEvolutions) {
|
for (const fe of fusionEvolutions) {
|
||||||
if (fe.validate(this)) {
|
if (fe.validate(this, true)) {
|
||||||
return fe;
|
return fe;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2775,7 +2774,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* This causes problems when there are intentional duplicates (i.e. Smeargle with Sketch)
|
* This causes problems when there are intentional duplicates (i.e. Smeargle with Sketch)
|
||||||
*/
|
*/
|
||||||
if (levelMoves) {
|
if (levelMoves) {
|
||||||
this.getUniqueMoves(levelMoves, ret);
|
Pokemon.getUniqueMoves(levelMoves, ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
@ -2789,7 +2788,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* @param levelMoves the input array to search for non-duplicates from
|
* @param levelMoves the input array to search for non-duplicates from
|
||||||
* @param ret the output array to be pushed into.
|
* @param ret the output array to be pushed into.
|
||||||
*/
|
*/
|
||||||
private getUniqueMoves(levelMoves: LevelMoves, ret: LevelMoves): void {
|
private static getUniqueMoves(levelMoves: LevelMoves, ret: LevelMoves): void {
|
||||||
const uniqueMoves: MoveId[] = [];
|
const uniqueMoves: MoveId[] = [];
|
||||||
for (const lm of levelMoves) {
|
for (const lm of levelMoves) {
|
||||||
if (!uniqueMoves.find(m => m === lm[1])) {
|
if (!uniqueMoves.find(m => m === lm[1])) {
|
||||||
@ -3047,294 +3046,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
/** Generates a semi-random moveset for a Pokemon */
|
/** Generates a semi-random moveset for a Pokemon */
|
||||||
public generateAndPopulateMoveset(): void {
|
public generateAndPopulateMoveset(): void {
|
||||||
this.moveset = [];
|
generateMoveset(this);
|
||||||
let movePool: [MoveId, number][] = [];
|
|
||||||
const allLevelMoves = this.getLevelMoves(1, true, true, this.hasTrainer());
|
|
||||||
if (!allLevelMoves) {
|
|
||||||
console.warn("Error encountered trying to generate moveset for:", this.species.name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const levelMove of allLevelMoves) {
|
|
||||||
if (this.level < levelMove[0]) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let weight = levelMove[0] + 20;
|
|
||||||
// Evolution Moves
|
|
||||||
if (levelMove[0] === EVOLVE_MOVE) {
|
|
||||||
weight = 70;
|
|
||||||
}
|
|
||||||
// Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight. Trainers use actual relearn moves.
|
|
||||||
if (
|
|
||||||
(levelMove[0] === 1 && allMoves[levelMove[1]].power >= 80)
|
|
||||||
|| (levelMove[0] === RELEARN_MOVE && this.hasTrainer())
|
|
||||||
) {
|
|
||||||
weight = 60;
|
|
||||||
}
|
|
||||||
if (!movePool.some(m => m[0] === levelMove[1]) && !allMoves[levelMove[1]].name.endsWith(" (N)")) {
|
|
||||||
movePool.push([levelMove[1], weight]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.hasTrainer()) {
|
|
||||||
const tms = Object.keys(tmSpecies);
|
|
||||||
for (const tm of tms) {
|
|
||||||
const moveId = Number.parseInt(tm) as MoveId;
|
|
||||||
let compatible = false;
|
|
||||||
for (const p of tmSpecies[tm]) {
|
|
||||||
if (Array.isArray(p)) {
|
|
||||||
if (
|
|
||||||
p[0] === this.species.speciesId
|
|
||||||
|| (this.fusionSpecies
|
|
||||||
&& p[0] === this.fusionSpecies.speciesId
|
|
||||||
&& p.slice(1).indexOf(this.species.forms[this.formIndex]) > -1)
|
|
||||||
) {
|
|
||||||
compatible = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (p === this.species.speciesId || (this.fusionSpecies && p === this.fusionSpecies.speciesId)) {
|
|
||||||
compatible = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (compatible && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
|
|
||||||
if (tmPoolTiers[moveId] === ModifierTier.COMMON && this.level >= 15) {
|
|
||||||
movePool.push([moveId, 24]);
|
|
||||||
} else if (tmPoolTiers[moveId] === ModifierTier.GREAT && this.level >= 30) {
|
|
||||||
movePool.push([moveId, 28]);
|
|
||||||
} else if (tmPoolTiers[moveId] === ModifierTier.ULTRA && this.level >= 50) {
|
|
||||||
movePool.push([moveId, 34]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No egg moves below level 60
|
|
||||||
if (this.level >= 60) {
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const moveId = speciesEggMoves[this.species.getRootSpeciesId()][i];
|
|
||||||
if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
|
|
||||||
movePool.push([moveId, 60]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const moveId = speciesEggMoves[this.species.getRootSpeciesId()][3];
|
|
||||||
// No rare egg moves before e4
|
|
||||||
if (
|
|
||||||
this.level >= 170
|
|
||||||
&& !movePool.some(m => m[0] === moveId)
|
|
||||||
&& !allMoves[moveId].name.endsWith(" (N)")
|
|
||||||
&& !this.isBoss()
|
|
||||||
) {
|
|
||||||
movePool.push([moveId, 50]);
|
|
||||||
}
|
|
||||||
if (this.fusionSpecies) {
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][i];
|
|
||||||
if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
|
|
||||||
movePool.push([moveId, 60]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][3];
|
|
||||||
// No rare egg moves before e4
|
|
||||||
if (
|
|
||||||
this.level >= 170
|
|
||||||
&& !movePool.some(m => m[0] === moveId)
|
|
||||||
&& !allMoves[moveId].name.endsWith(" (N)")
|
|
||||||
&& !this.isBoss()
|
|
||||||
) {
|
|
||||||
movePool.push([moveId, 50]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bosses never get self ko moves or Pain Split
|
|
||||||
if (this.isBoss()) {
|
|
||||||
movePool = movePool.filter(
|
|
||||||
m => !allMoves[m[0]].hasAttr("SacrificialAttr") && !allMoves[m[0]].hasAttr("HpSplitAttr"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// No one gets Memento or Final Gambit
|
|
||||||
movePool = movePool.filter(m => !allMoves[m[0]].hasAttr("SacrificialAttrOnHit"));
|
|
||||||
if (this.hasTrainer()) {
|
|
||||||
// Trainers never get OHKO moves
|
|
||||||
movePool = movePool.filter(m => !allMoves[m[0]].hasAttr("OneHitKOAttr"));
|
|
||||||
// Half the weight of self KO moves
|
|
||||||
movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].hasAttr("SacrificialAttr") ? 0.5 : 1)]);
|
|
||||||
// Trainers get a weight bump to stat buffing moves
|
|
||||||
movePool = movePool.map(m => [
|
|
||||||
m[0],
|
|
||||||
m[1] * (allMoves[m[0]].getAttrs("StatStageChangeAttr").some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1),
|
|
||||||
]);
|
|
||||||
// Trainers get a weight decrease to multiturn moves
|
|
||||||
movePool = movePool.map(m => [
|
|
||||||
m[0],
|
|
||||||
m[1] * (!!allMoves[m[0]].isChargingMove() || !!allMoves[m[0]].hasAttr("RechargeAttr") ? 0.7 : 1),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Weight towards higher power moves, by reducing the power of moves below the highest power.
|
|
||||||
// Caps max power at 90 to avoid something like hyper beam ruining the stats.
|
|
||||||
// This is a pretty soft weighting factor, although it is scaled with the weight multiplier.
|
|
||||||
const maxPower = Math.min(
|
|
||||||
movePool.reduce((v, m) => Math.max(allMoves[m[0]].calculateEffectivePower(), v), 40),
|
|
||||||
90,
|
|
||||||
);
|
|
||||||
movePool = movePool.map(m => [
|
|
||||||
m[0],
|
|
||||||
m[1]
|
|
||||||
* (allMoves[m[0]].category === MoveCategory.STATUS
|
|
||||||
? 1
|
|
||||||
: Math.max(Math.min(allMoves[m[0]].calculateEffectivePower() / maxPower, 1), 0.5)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Weight damaging moves against the lower stat. This uses a non-linear relationship.
|
|
||||||
// If the higher stat is 1 - 1.09x higher, no change. At higher stat ~1.38x lower stat, off-stat moves have half weight.
|
|
||||||
// One third weight at ~1.58x higher, one quarter weight at ~1.73x higher, one fifth at ~1.87x, and one tenth at ~2.35x higher.
|
|
||||||
const atk = this.getStat(Stat.ATK);
|
|
||||||
const spAtk = this.getStat(Stat.SPATK);
|
|
||||||
const worseCategory: MoveCategory = atk > spAtk ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL;
|
|
||||||
const statRatio = worseCategory === MoveCategory.PHYSICAL ? atk / spAtk : spAtk / atk;
|
|
||||||
movePool = movePool.map(m => [
|
|
||||||
m[0],
|
|
||||||
m[1] * (allMoves[m[0]].category === worseCategory ? Math.min(Math.pow(statRatio, 3) * 1.3, 1) : 1),
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** The higher this is the more the game weights towards higher level moves. At `0` all moves are equal weight. */
|
|
||||||
let weightMultiplier = 1.6;
|
|
||||||
if (this.isBoss()) {
|
|
||||||
weightMultiplier += 0.4;
|
|
||||||
}
|
|
||||||
const baseWeights: [MoveId, number][] = movePool.map(m => [
|
|
||||||
m[0],
|
|
||||||
Math.ceil(Math.pow(m[1], weightMultiplier) * 100),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const STAB_BLACKLIST: ReadonlySet<MoveId> = new Set([
|
|
||||||
MoveId.BEAT_UP,
|
|
||||||
MoveId.BELCH,
|
|
||||||
MoveId.BIDE,
|
|
||||||
MoveId.COMEUPPANCE,
|
|
||||||
MoveId.COUNTER,
|
|
||||||
MoveId.DOOM_DESIRE,
|
|
||||||
MoveId.DRAGON_RAGE,
|
|
||||||
MoveId.DREAM_EATER,
|
|
||||||
MoveId.ENDEAVOR,
|
|
||||||
MoveId.EXPLOSION,
|
|
||||||
MoveId.FAKE_OUT,
|
|
||||||
MoveId.FIRST_IMPRESSION,
|
|
||||||
MoveId.FISSURE,
|
|
||||||
MoveId.FLING,
|
|
||||||
MoveId.FOCUS_PUNCH,
|
|
||||||
MoveId.FUTURE_SIGHT,
|
|
||||||
MoveId.GUILLOTINE,
|
|
||||||
MoveId.HOLD_BACK,
|
|
||||||
MoveId.HORN_DRILL,
|
|
||||||
MoveId.LAST_RESORT,
|
|
||||||
MoveId.METAL_BURST,
|
|
||||||
MoveId.MIRROR_COAT,
|
|
||||||
MoveId.MISTY_EXPLOSION,
|
|
||||||
MoveId.NATURAL_GIFT,
|
|
||||||
MoveId.NATURES_MADNESS,
|
|
||||||
MoveId.NIGHT_SHADE,
|
|
||||||
MoveId.PSYWAVE,
|
|
||||||
MoveId.RUINATION,
|
|
||||||
MoveId.SELF_DESTRUCT,
|
|
||||||
MoveId.SHEER_COLD,
|
|
||||||
MoveId.SHELL_TRAP,
|
|
||||||
MoveId.SKY_DROP,
|
|
||||||
MoveId.SNORE,
|
|
||||||
MoveId.SONIC_BOOM,
|
|
||||||
MoveId.SPIT_UP,
|
|
||||||
MoveId.STEEL_BEAM,
|
|
||||||
MoveId.STEEL_ROLLER,
|
|
||||||
MoveId.SUPER_FANG,
|
|
||||||
MoveId.SYNCHRONOISE,
|
|
||||||
MoveId.UPPER_HAND,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// All Pokemon force a STAB move first
|
|
||||||
const stabMovePool = baseWeights.filter(
|
|
||||||
m =>
|
|
||||||
allMoves[m[0]].category !== MoveCategory.STATUS
|
|
||||||
&& this.isOfType(allMoves[m[0]].type)
|
|
||||||
&& !STAB_BLACKLIST.has(m[0]),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (stabMovePool.length > 0) {
|
|
||||||
const totalWeight = stabMovePool.reduce((v, m) => v + m[1], 0);
|
|
||||||
let rand = randSeedInt(totalWeight);
|
|
||||||
let index = 0;
|
|
||||||
while (rand > stabMovePool[index][1]) {
|
|
||||||
rand -= stabMovePool[index++][1];
|
|
||||||
}
|
|
||||||
this.moveset.push(new PokemonMove(stabMovePool[index][0]));
|
|
||||||
} else {
|
|
||||||
// If there are no damaging STAB moves, just force a random damaging move
|
|
||||||
const attackMovePool = baseWeights.filter(
|
|
||||||
m => allMoves[m[0]].category !== MoveCategory.STATUS && !STAB_BLACKLIST.has(m[0]),
|
|
||||||
);
|
|
||||||
if (attackMovePool.length > 0) {
|
|
||||||
const totalWeight = attackMovePool.reduce((v, m) => v + m[1], 0);
|
|
||||||
let rand = randSeedInt(totalWeight);
|
|
||||||
let index = 0;
|
|
||||||
while (rand > attackMovePool[index][1]) {
|
|
||||||
rand -= attackMovePool[index++][1];
|
|
||||||
}
|
|
||||||
this.moveset.push(new PokemonMove(attackMovePool[index][0], 0, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (baseWeights.length > this.moveset.length && this.moveset.length < 4) {
|
|
||||||
if (this.hasTrainer()) {
|
|
||||||
// Sqrt the weight of any damaging moves with overlapping types. This is about a 0.05 - 0.1 multiplier.
|
|
||||||
// Other damaging moves 2x weight if 0-1 damaging moves, 0.5x if 2, 0.125x if 3. These weights get 20x if STAB.
|
|
||||||
// Status moves remain unchanged on weight, this encourages 1-2
|
|
||||||
movePool = baseWeights
|
|
||||||
.filter(
|
|
||||||
m =>
|
|
||||||
!this.moveset.some(
|
|
||||||
mo =>
|
|
||||||
m[0] === mo.moveId
|
|
||||||
|| (allMoves[m[0]].hasAttr("SacrificialAttr") && mo.getMove().hasAttr("SacrificialAttr")), // Only one self-KO move allowed
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.map(m => {
|
|
||||||
let ret: number;
|
|
||||||
if (
|
|
||||||
this.moveset.some(
|
|
||||||
mo => mo.getMove().category !== MoveCategory.STATUS && mo.getMove().type === allMoves[m[0]].type,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
ret = Math.ceil(Math.sqrt(m[1]));
|
|
||||||
} else if (allMoves[m[0]].category !== MoveCategory.STATUS) {
|
|
||||||
ret = Math.ceil(
|
|
||||||
(m[1] / Math.max(Math.pow(4, this.moveset.filter(mo => (mo.getMove().power ?? 0) > 1).length) / 8, 0.5))
|
|
||||||
* (this.isOfType(allMoves[m[0]].type) && !STAB_BLACKLIST.has(m[0]) ? 20 : 1),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ret = m[1];
|
|
||||||
}
|
|
||||||
return [m[0], ret];
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Non-trainer pokemon just use normal weights
|
|
||||||
movePool = baseWeights.filter(
|
|
||||||
m =>
|
|
||||||
!this.moveset.some(
|
|
||||||
mo =>
|
|
||||||
m[0] === mo.moveId
|
|
||||||
|| (allMoves[m[0]].hasAttr("SacrificialAttr") && mo.getMove().hasAttr("SacrificialAttr")), // Only one self-KO move allowed
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const totalWeight = movePool.reduce((v, m) => v + m[1], 0);
|
|
||||||
let rand = randSeedInt(totalWeight);
|
|
||||||
let index = 0;
|
|
||||||
while (rand > movePool[index][1]) {
|
|
||||||
rand -= movePool[index++][1];
|
|
||||||
}
|
|
||||||
this.moveset.push(new PokemonMove(movePool[index][0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger FormChange, except for enemy Pokemon during Mystery Encounters, to avoid crashes
|
// Trigger FormChange, except for enemy Pokemon during Mystery Encounters, to avoid crashes
|
||||||
if (
|
if (
|
||||||
@ -3606,7 +3318,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const ally = this.getAlly();
|
const ally = this.getAlly();
|
||||||
if (!isNullOrUndefined(ally)) {
|
if (ally != null) {
|
||||||
const ignore =
|
const ignore =
|
||||||
this.hasAbilityWithAttr("MoveAbilityBypassAbAttr") || sourceMove.hasFlag(MoveFlags.IGNORE_ABILITIES);
|
this.hasAbilityWithAttr("MoveAbilityBypassAbAttr") || sourceMove.hasFlag(MoveFlags.IGNORE_ABILITIES);
|
||||||
applyAbAttrs("AllyStatMultiplierAbAttr", {
|
applyAbAttrs("AllyStatMultiplierAbAttr", {
|
||||||
@ -4023,7 +3735,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
const ally = this.getAlly();
|
const ally = this.getAlly();
|
||||||
/** Additionally apply friend guard damage reduction if ally has it. */
|
/** Additionally apply friend guard damage reduction if ally has it. */
|
||||||
if (globalScene.currentBattle.double && !isNullOrUndefined(ally) && ally.isActive(true)) {
|
if (globalScene.currentBattle.double && ally != null && ally.isActive(true)) {
|
||||||
applyAbAttrs("AlliedFieldDamageReductionAbAttr", {
|
applyAbAttrs("AlliedFieldDamageReductionAbAttr", {
|
||||||
...abAttrParams,
|
...abAttrParams,
|
||||||
// Same parameters as before, except we are applying the ally's ability
|
// Same parameters as before, except we are applying the ally's ability
|
||||||
@ -6405,13 +6117,13 @@ export class EnemyPokemon extends Pokemon {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
speciesId in Overrides.ENEMY_FORM_OVERRIDES
|
speciesId in Overrides.ENEMY_FORM_OVERRIDES
|
||||||
&& !isNullOrUndefined(Overrides.ENEMY_FORM_OVERRIDES[speciesId])
|
&& Overrides.ENEMY_FORM_OVERRIDES[speciesId] != null
|
||||||
&& this.species.forms[Overrides.ENEMY_FORM_OVERRIDES[speciesId]]
|
&& this.species.forms[Overrides.ENEMY_FORM_OVERRIDES[speciesId]]
|
||||||
) {
|
) {
|
||||||
this.formIndex = Overrides.ENEMY_FORM_OVERRIDES[speciesId];
|
this.formIndex = Overrides.ENEMY_FORM_OVERRIDES[speciesId];
|
||||||
} else if (globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) {
|
} else if (globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) {
|
||||||
const eventBoss = getDailyEventSeedBoss(globalScene.seed);
|
const eventBoss = getDailyEventSeedBoss(globalScene.seed);
|
||||||
if (!isNullOrUndefined(eventBoss)) {
|
if (eventBoss != null) {
|
||||||
this.formIndex = eventBoss.formIndex;
|
this.formIndex = eventBoss.formIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6947,12 +6659,20 @@ export class EnemyPokemon extends Pokemon {
|
|||||||
* @param segmentIndex index of the segment to get down to (0 = no shield left, 1 = 1 shield left, etc.)
|
* @param segmentIndex index of the segment to get down to (0 = no shield left, 1 = 1 shield left, etc.)
|
||||||
*/
|
*/
|
||||||
handleBossSegmentCleared(segmentIndex: number): void {
|
handleBossSegmentCleared(segmentIndex: number): void {
|
||||||
|
let doStatBoost = !this.hasTrainer();
|
||||||
|
// TODO: Rewrite this bespoke logic to improve clarity
|
||||||
while (this.bossSegmentIndex > 0 && segmentIndex - 1 < this.bossSegmentIndex) {
|
while (this.bossSegmentIndex > 0 && segmentIndex - 1 < this.bossSegmentIndex) {
|
||||||
|
this.bossSegmentIndex--;
|
||||||
|
|
||||||
|
// Continue, _not_ break here, to ensure that each segment is still broken
|
||||||
|
if (!doStatBoost) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let boostedStat: EffectiveStat | undefined;
|
||||||
// Filter out already maxed out stat stages and weigh the rest based on existing stats
|
// Filter out already maxed out stat stages and weigh the rest based on existing stats
|
||||||
const leftoverStats = EFFECTIVE_STATS.filter((s: EffectiveStat) => this.getStatStage(s) < 6);
|
const leftoverStats = EFFECTIVE_STATS.filter((s: EffectiveStat) => this.getStatStage(s) < 6);
|
||||||
const statWeights = leftoverStats.map((s: EffectiveStat) => this.getStat(s, false));
|
const statWeights = leftoverStats.map((s: EffectiveStat) => this.getStat(s, false));
|
||||||
|
|
||||||
let boostedStat: EffectiveStat | undefined;
|
|
||||||
const statThresholds: number[] = [];
|
const statThresholds: number[] = [];
|
||||||
let totalWeight = 0;
|
let totalWeight = 0;
|
||||||
|
|
||||||
@ -6971,18 +6691,18 @@ export class EnemyPokemon extends Pokemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (boostedStat === undefined) {
|
if (boostedStat === undefined) {
|
||||||
this.bossSegmentIndex--;
|
doStatBoost = false;
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let stages = 1;
|
let stages = 1;
|
||||||
|
|
||||||
// increase the boost if the boss has at least 3 segments and we passed last shield
|
// increase the boost if the boss has at least 3 segments and we passed last shield
|
||||||
if (this.bossSegments >= 3 && this.bossSegmentIndex === 1) {
|
if (this.bossSegments >= 3 && this.bossSegmentIndex === 0) {
|
||||||
stages++;
|
stages++;
|
||||||
}
|
}
|
||||||
// increase the boost if the boss has at least 5 segments and we passed the second to last shield
|
// increase the boost if the boss has at least 5 segments and we passed the second to last shield
|
||||||
if (this.bossSegments >= 5 && this.bossSegmentIndex === 2) {
|
if (this.bossSegments >= 5 && this.bossSegmentIndex === 1) {
|
||||||
stages++;
|
stages++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6995,7 +6715,6 @@ export class EnemyPokemon extends Pokemon {
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
this.bossSegmentIndex--;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import { SpeciesId } from "#enums/species-id";
|
|||||||
import type { Arena } from "#field/arena";
|
import type { Arena } from "#field/arena";
|
||||||
import { classicFixedBattles, type FixedBattleConfigs } from "#trainers/fixed-battle-configs";
|
import { classicFixedBattles, type FixedBattleConfigs } from "#trainers/fixed-battle-configs";
|
||||||
import { applyChallenges } from "#utils/challenge-utils";
|
import { applyChallenges } from "#utils/challenge-utils";
|
||||||
import { BooleanHolder, isNullOrUndefined, randSeedInt, randSeedItem } from "#utils/common";
|
import { BooleanHolder, randSeedInt, randSeedItem } from "#utils/common";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ export class GameMode implements GameModeConfig {
|
|||||||
* - Town
|
* - Town
|
||||||
*/
|
*/
|
||||||
getStartingBiome(): BiomeId {
|
getStartingBiome(): BiomeId {
|
||||||
if (!isNullOrUndefined(Overrides.STARTING_BIOME_OVERRIDE)) {
|
if (Overrides.STARTING_BIOME_OVERRIDE != null) {
|
||||||
return Overrides.STARTING_BIOME_OVERRIDE;
|
return Overrides.STARTING_BIOME_OVERRIDE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,7 +234,7 @@ export class GameMode implements GameModeConfig {
|
|||||||
getOverrideSpecies(waveIndex: number): PokemonSpecies | null {
|
getOverrideSpecies(waveIndex: number): PokemonSpecies | null {
|
||||||
if (this.isDaily && this.isWaveFinal(waveIndex)) {
|
if (this.isDaily && this.isWaveFinal(waveIndex)) {
|
||||||
const eventBoss = getDailyEventSeedBoss(globalScene.seed);
|
const eventBoss = getDailyEventSeedBoss(globalScene.seed);
|
||||||
if (!isNullOrUndefined(eventBoss)) {
|
if (eventBoss != null) {
|
||||||
// Cannot set form index here, it will be overriden when adding it as enemy pokemon.
|
// Cannot set form index here, it will be overriden when adding it as enemy pokemon.
|
||||||
return getPokemonSpecies(eventBoss.speciesId);
|
return getPokemonSpecies(eventBoss.speciesId);
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,6 @@ import {
|
|||||||
} from "#modifiers/modifier-pools";
|
} from "#modifiers/modifier-pools";
|
||||||
import { WeightedModifierType } from "#modifiers/modifier-type";
|
import { WeightedModifierType } from "#modifiers/modifier-type";
|
||||||
import type { WeightedModifierTypeWeightFunc } from "#types/modifier-types";
|
import type { WeightedModifierTypeWeightFunc } from "#types/modifier-types";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the wild modifier pool
|
* Initialize the wild modifier pool
|
||||||
@ -409,7 +408,7 @@ function initUltraModifierPool() {
|
|||||||
if (!isHoldingOrb) {
|
if (!isHoldingOrb) {
|
||||||
const moveset = p
|
const moveset = p
|
||||||
.getMoveset(true)
|
.getMoveset(true)
|
||||||
.filter(m => !isNullOrUndefined(m))
|
.filter(m => m != null)
|
||||||
.map(m => m.moveId);
|
.map(m => m.moveId);
|
||||||
const canSetStatus = p.canSetStatus(StatusEffect.TOXIC, true, true, null, true);
|
const canSetStatus = p.canSetStatus(StatusEffect.TOXIC, true, true, null, true);
|
||||||
|
|
||||||
@ -455,7 +454,7 @@ function initUltraModifierPool() {
|
|||||||
if (!isHoldingOrb) {
|
if (!isHoldingOrb) {
|
||||||
const moveset = p
|
const moveset = p
|
||||||
.getMoveset(true)
|
.getMoveset(true)
|
||||||
.filter(m => !isNullOrUndefined(m))
|
.filter(m => m != null)
|
||||||
.map(m => m.moveId);
|
.map(m => m.moveId);
|
||||||
const canSetStatus = p.canSetStatus(StatusEffect.BURN, true, true, null, true);
|
const canSetStatus = p.canSetStatus(StatusEffect.BURN, true, true, null, true);
|
||||||
|
|
||||||
|
@ -119,15 +119,7 @@ import type { PokemonMoveSelectFilter, PokemonSelectFilter } from "#ui/party-ui-
|
|||||||
import { PartyUiHandler } from "#ui/party-ui-handler";
|
import { PartyUiHandler } from "#ui/party-ui-handler";
|
||||||
import { getModifierTierTextTint } from "#ui/text";
|
import { getModifierTierTextTint } from "#ui/text";
|
||||||
import { applyChallenges } from "#utils/challenge-utils";
|
import { applyChallenges } from "#utils/challenge-utils";
|
||||||
import {
|
import { BooleanHolder, formatMoney, NumberHolder, padInt, randSeedInt, randSeedItem } from "#utils/common";
|
||||||
BooleanHolder,
|
|
||||||
formatMoney,
|
|
||||||
isNullOrUndefined,
|
|
||||||
NumberHolder,
|
|
||||||
padInt,
|
|
||||||
randSeedInt,
|
|
||||||
randSeedItem,
|
|
||||||
} from "#utils/common";
|
|
||||||
import { getEnumKeys, getEnumValues } from "#utils/enums";
|
import { getEnumKeys, getEnumValues } from "#utils/enums";
|
||||||
import { getModifierPoolForType, getModifierType } from "#utils/modifier-utils";
|
import { getModifierPoolForType, getModifierType } from "#utils/modifier-utils";
|
||||||
import { toCamelCase } from "#utils/strings";
|
import { toCamelCase } from "#utils/strings";
|
||||||
@ -263,7 +255,7 @@ export class ModifierType {
|
|||||||
this.tier = modifier.modifierType.tier;
|
this.tier = modifier.modifierType.tier;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
if (isNullOrUndefined(defaultTier)) {
|
if (defaultTier == null) {
|
||||||
// If weight is 0, keep track of the first tier where the item was found
|
// If weight is 0, keep track of the first tier where the item was found
|
||||||
defaultTier = modifier.modifierType.tier;
|
defaultTier = modifier.modifierType.tier;
|
||||||
}
|
}
|
||||||
@ -2920,7 +2912,7 @@ export function getPartyLuckValue(party: Pokemon[]): number {
|
|||||||
globalScene.executeWithSeedOffset(
|
globalScene.executeWithSeedOffset(
|
||||||
() => {
|
() => {
|
||||||
const eventLuck = getDailyEventSeedLuck(globalScene.seed);
|
const eventLuck = getDailyEventSeedLuck(globalScene.seed);
|
||||||
if (!isNullOrUndefined(eventLuck)) {
|
if (eventLuck != null) {
|
||||||
DailyLuck.value = eventLuck;
|
DailyLuck.value = eventLuck;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ import type {
|
|||||||
import type { VoucherType } from "#system/voucher";
|
import type { VoucherType } from "#system/voucher";
|
||||||
import type { ModifierInstanceMap, ModifierString } from "#types/modifier-types";
|
import type { ModifierInstanceMap, ModifierString } from "#types/modifier-types";
|
||||||
import { addTextObject } from "#ui/text";
|
import { addTextObject } from "#ui/text";
|
||||||
import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, randSeedFloat, toDmgValue } from "#utils/common";
|
import { BooleanHolder, hslToHex, NumberHolder, randSeedFloat, toDmgValue } from "#utils/common";
|
||||||
import { getModifierType } from "#utils/modifier-utils";
|
import { getModifierType } from "#utils/modifier-utils";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
@ -728,7 +728,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPokemon(): Pokemon | undefined {
|
getPokemon(): Pokemon | undefined {
|
||||||
return globalScene.getPokemonById(this.pokemonId) ?? undefined;
|
return globalScene.getPokemonById(this.pokemonId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getScoreMultiplier(): number {
|
getScoreMultiplier(): number {
|
||||||
@ -2113,10 +2113,7 @@ export class PokemonHpRestoreModifier extends ConsumablePokemonModifier {
|
|||||||
* @returns `true` if the {@linkcode PokemonHpRestoreModifier} should be applied
|
* @returns `true` if the {@linkcode PokemonHpRestoreModifier} should be applied
|
||||||
*/
|
*/
|
||||||
override shouldApply(playerPokemon?: PlayerPokemon, multiplier?: number): boolean {
|
override shouldApply(playerPokemon?: PlayerPokemon, multiplier?: number): boolean {
|
||||||
return (
|
return super.shouldApply(playerPokemon) && (this.fainted || (multiplier != null && typeof multiplier === "number"));
|
||||||
super.shouldApply(playerPokemon)
|
|
||||||
&& (this.fainted || (!isNullOrUndefined(multiplier) && typeof multiplier === "number"))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2753,10 +2750,10 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isNullOrUndefined(count)) {
|
if (count != null) {
|
||||||
return this.applyHitCountBoost(count);
|
return this.applyHitCountBoost(count);
|
||||||
}
|
}
|
||||||
if (!isNullOrUndefined(damageMultiplier)) {
|
if (damageMultiplier != null) {
|
||||||
return this.applyDamageModifier(pokemon, damageMultiplier);
|
return this.applyDamageModifier(pokemon, damageMultiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { type PokeballCounts } from "#app/battle-scene";
|
import type { PokeballCounts } from "#app/battle-scene";
|
||||||
import { EvolutionItem } from "#balance/pokemon-evolutions";
|
import { EvolutionItem } from "#balance/pokemon-evolutions";
|
||||||
import { Gender } from "#data/gender";
|
import { Gender } from "#data/gender";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
@ -18,10 +18,11 @@ import { Stat } from "#enums/stat";
|
|||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
import { TimeOfDay } from "#enums/time-of-day";
|
import { TimeOfDay } from "#enums/time-of-day";
|
||||||
import { TrainerType } from "#enums/trainer-type";
|
import { TrainerType } from "#enums/trainer-type";
|
||||||
|
import { TrainerVariant } from "#enums/trainer-variant";
|
||||||
import { Unlockables } from "#enums/unlockables";
|
import { Unlockables } from "#enums/unlockables";
|
||||||
import { VariantTier } from "#enums/variant-tier";
|
import { VariantTier } from "#enums/variant-tier";
|
||||||
import { WeatherType } from "#enums/weather-type";
|
import { WeatherType } from "#enums/weather-type";
|
||||||
import { type ModifierOverride } from "#modifiers/modifier-type";
|
import type { ModifierOverride } from "#modifiers/modifier-type";
|
||||||
import { Variant } from "#sprites/variant";
|
import { Variant } from "#sprites/variant";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -311,8 +312,12 @@ export type BattleStyle = "double" | "single" | "even-doubles" | "odd-doubles";
|
|||||||
export type RandomTrainerOverride = {
|
export type RandomTrainerOverride = {
|
||||||
/** The Type of trainer to force */
|
/** The Type of trainer to force */
|
||||||
trainerType: Exclude<TrainerType, TrainerType.UNKNOWN>;
|
trainerType: Exclude<TrainerType, TrainerType.UNKNOWN>;
|
||||||
/* If the selected trainer type has a double version, it will always use its double version. */
|
/**
|
||||||
alwaysDouble?: boolean;
|
* The {@linkcode TrainerVariant} to force.
|
||||||
|
* @remarks
|
||||||
|
* `TrainerVariant.DOUBLE` cannot be forced on the first wave of a game due to issues with trainer party generation.
|
||||||
|
*/
|
||||||
|
trainerVariant?: TrainerVariant;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The type of the {@linkcode DefaultOverrides} class */
|
/** The type of the {@linkcode DefaultOverrides} class */
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Manager for phases used by battle scene.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* **This file must not be imported or used directly.**
|
||||||
|
* The manager is exclusively used by the Battle Scene and is NOT intended for external use.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
import { PHASE_START_COLOR } from "#app/constants/colors";
|
import { PHASE_START_COLOR } from "#app/constants/colors";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import type { Phase } from "#app/phase";
|
import type { Phase } from "#app/phase";
|
||||||
@ -104,15 +113,6 @@ import { WeatherEffectPhase } from "#phases/weather-effect-phase";
|
|||||||
import type { PhaseMap, PhaseString } from "#types/phase-types";
|
import type { PhaseMap, PhaseString } from "#types/phase-types";
|
||||||
import { type Constructor, coerceArray } from "#utils/common";
|
import { type Constructor, coerceArray } from "#utils/common";
|
||||||
|
|
||||||
/**
|
|
||||||
* @module
|
|
||||||
* Manager for phases used by battle scene.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* **This file must not be imported or used directly.**
|
|
||||||
* The manager is exclusively used by the Battle Scene and is NOT intended for external use.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object that holds all of the phase constructors.
|
* Object that holds all of the phase constructors.
|
||||||
* This is used to create new phases dynamically using the `newPhase` method in the `PhaseManager`.
|
* This is used to create new phases dynamically using the `newPhase` method in the `PhaseManager`.
|
||||||
|
@ -17,7 +17,6 @@ import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon";
|
|||||||
import { PokemonInstantReviveModifier } from "#modifiers/modifier";
|
import { PokemonInstantReviveModifier } from "#modifiers/modifier";
|
||||||
import { PokemonMove } from "#moves/pokemon-move";
|
import { PokemonMove } from "#moves/pokemon-move";
|
||||||
import { PokemonPhase } from "#phases/pokemon-phase";
|
import { PokemonPhase } from "#phases/pokemon-phase";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export class FaintPhase extends PokemonPhase {
|
export class FaintPhase extends PokemonPhase {
|
||||||
@ -187,7 +186,7 @@ export class FaintPhase extends PokemonPhase {
|
|||||||
|
|
||||||
// in double battles redirect potential moves off fainted pokemon
|
// in double battles redirect potential moves off fainted pokemon
|
||||||
const allyPokemon = pokemon.getAlly();
|
const allyPokemon = pokemon.getAlly();
|
||||||
if (globalScene.currentBattle.double && !isNullOrUndefined(allyPokemon)) {
|
if (globalScene.currentBattle.double && allyPokemon != null) {
|
||||||
globalScene.redirectPokemonMoves(pokemon, allyPokemon);
|
globalScene.redirectPokemonMoves(pokemon, allyPokemon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ export class LoginPhase extends Phase {
|
|||||||
globalScene.ui.showText(i18next.t("menu:logInOrCreateAccount"));
|
globalScene.ui.showText(i18next.t("menu:logInOrCreateAccount"));
|
||||||
}
|
}
|
||||||
|
|
||||||
globalScene.playSound("menu_open");
|
globalScene.playSound("ui/menu_open");
|
||||||
|
|
||||||
const loadData = () => {
|
const loadData = () => {
|
||||||
updateUserInfo().then(success => {
|
updateUserInfo().then(success => {
|
||||||
@ -53,7 +53,7 @@ export class LoginPhase extends Phase {
|
|||||||
loadData();
|
loadData();
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
globalScene.playSound("menu_open");
|
globalScene.playSound("ui/menu_open");
|
||||||
globalScene.ui.setMode(UiMode.REGISTRATION_FORM, {
|
globalScene.ui.setMode(UiMode.REGISTRATION_FORM, {
|
||||||
buttonActions: [
|
buttonActions: [
|
||||||
() => {
|
() => {
|
||||||
|
@ -38,7 +38,7 @@ import { DamageAchv } from "#system/achv";
|
|||||||
import type { DamageResult } from "#types/damage-result";
|
import type { DamageResult } from "#types/damage-result";
|
||||||
import type { TurnMove } from "#types/turn-move";
|
import type { TurnMove } from "#types/turn-move";
|
||||||
import type { nil } from "#utils/common";
|
import type { nil } from "#utils/common";
|
||||||
import { BooleanHolder, isNullOrUndefined, NumberHolder } from "#utils/common";
|
import { BooleanHolder, NumberHolder } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
|
export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
|
||||||
@ -542,7 +542,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
(attr: MoveAttr) =>
|
(attr: MoveAttr) =>
|
||||||
attr.is("MoveEffectAttr")
|
attr.is("MoveEffectAttr")
|
||||||
&& attr.trigger === triggerType
|
&& attr.trigger === triggerType
|
||||||
&& (isNullOrUndefined(selfTarget) || attr.selfTarget === selfTarget)
|
&& (selfTarget == null || attr.selfTarget === selfTarget)
|
||||||
&& (!attr.firstHitOnly || this.firstHit)
|
&& (!attr.firstHitOnly || this.firstHit)
|
||||||
&& (!attr.lastHitOnly || this.lastHit)
|
&& (!attr.lastHitOnly || this.lastHit)
|
||||||
&& (!attr.firstTargetOnly || (firstTarget ?? true)),
|
&& (!attr.firstTargetOnly || (firstTarget ?? true)),
|
||||||
@ -879,7 +879,8 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns The {@linkcode Pokemon} using this phase's invoked move.
|
* @returns The {@linkcode Pokemon} using this phase's invoked move.
|
||||||
* Is never null during the move execution itself, as {@linkcode start} ends the phase immediately if a source is missing.
|
* Is never nullish during the move execution itself, as the `start` method
|
||||||
|
* ends the phase immediately if a source is missing.
|
||||||
* @todo Delete in favor of {@linkcode PokemonPhase.getPokemon}
|
* @todo Delete in favor of {@linkcode PokemonPhase.getPokemon}
|
||||||
*/
|
*/
|
||||||
public getUserPokemon(): Pokemon {
|
public getUserPokemon(): Pokemon {
|
||||||
|
@ -14,7 +14,7 @@ import type { OptionSelectSettings } from "#mystery-encounters/encounter-phase-u
|
|||||||
import { transitionMysteryEncounterIntroVisuals } from "#mystery-encounters/encounter-phase-utils";
|
import { transitionMysteryEncounterIntroVisuals } from "#mystery-encounters/encounter-phase-utils";
|
||||||
import type { MysteryEncounterOption, OptionPhaseCallback } from "#mystery-encounters/mystery-encounter-option";
|
import type { MysteryEncounterOption, OptionPhaseCallback } from "#mystery-encounters/mystery-encounter-option";
|
||||||
import { SeenEncounterData } from "#mystery-encounters/mystery-encounter-save-data";
|
import { SeenEncounterData } from "#mystery-encounters/mystery-encounter-save-data";
|
||||||
import { isNullOrUndefined, randSeedItem } from "#utils/common";
|
import { randSeedItem } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,7 +93,7 @@ export class MysteryEncounterPhase extends Phase {
|
|||||||
if (option.onPreOptionPhase) {
|
if (option.onPreOptionPhase) {
|
||||||
globalScene.executeWithSeedOffset(async () => {
|
globalScene.executeWithSeedOffset(async () => {
|
||||||
return await option.onPreOptionPhase!().then(result => {
|
return await option.onPreOptionPhase!().then(result => {
|
||||||
if (isNullOrUndefined(result) || result) {
|
if (result == null || result) {
|
||||||
this.continueEncounter();
|
this.continueEncounter();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -578,7 +578,7 @@ export class PostMysteryEncounterPhase extends Phase {
|
|||||||
if (this.onPostOptionSelect) {
|
if (this.onPostOptionSelect) {
|
||||||
globalScene.executeWithSeedOffset(async () => {
|
globalScene.executeWithSeedOffset(async () => {
|
||||||
return await this.onPostOptionSelect!().then(result => {
|
return await this.onPostOptionSelect!().then(result => {
|
||||||
if (isNullOrUndefined(result) || result) {
|
if (result == null || result) {
|
||||||
this.continueEncounter();
|
this.continueEncounter();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -23,6 +23,7 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
|
|||||||
* @param sourceText - The text to show for the source of the status effect, if any; default `null`.
|
* @param sourceText - The text to show for the source of the status effect, if any; default `null`.
|
||||||
* @param statusMessage - A string containing text to be displayed upon status setting;
|
* @param statusMessage - A string containing text to be displayed upon status setting;
|
||||||
* defaults to normal key for status if empty or omitted.
|
* defaults to normal key for status if empty or omitted.
|
||||||
|
* @todo stop passing `null` to the phase
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
battlerIndex: BattlerIndex,
|
battlerIndex: BattlerIndex,
|
||||||
|
@ -4,7 +4,6 @@ import { PokemonAnimType } from "#enums/pokemon-anim-type";
|
|||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import type { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import { BattlePhase } from "#phases/battle-phase";
|
import { BattlePhase } from "#phases/battle-phase";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
|
|
||||||
export class PokemonAnimPhase extends BattlePhase {
|
export class PokemonAnimPhase extends BattlePhase {
|
||||||
public readonly phaseName = "PokemonAnimPhase";
|
public readonly phaseName = "PokemonAnimPhase";
|
||||||
@ -52,7 +51,7 @@ export class PokemonAnimPhase extends BattlePhase {
|
|||||||
|
|
||||||
private doSubstituteAddAnim(): void {
|
private doSubstituteAddAnim(): void {
|
||||||
const substitute = this.pokemon.getTag(SubstituteTag);
|
const substitute = this.pokemon.getTag(SubstituteTag);
|
||||||
if (isNullOrUndefined(substitute)) {
|
if (substitute == null) {
|
||||||
this.end();
|
this.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -336,7 +335,7 @@ export class PokemonAnimPhase extends BattlePhase {
|
|||||||
// Note: unlike the other Commander animation, this is played through the
|
// Note: unlike the other Commander animation, this is played through the
|
||||||
// Dondozo instead of the Tatsugiri.
|
// Dondozo instead of the Tatsugiri.
|
||||||
const tatsugiri = this.pokemon.getAlly();
|
const tatsugiri = this.pokemon.getAlly();
|
||||||
if (isNullOrUndefined(tatsugiri)) {
|
if (tatsugiri == null) {
|
||||||
console.warn("Aborting COMMANDER_REMOVE anim: Tatsugiri is undefined");
|
console.warn("Aborting COMMANDER_REMOVE anim: Tatsugiri is undefined");
|
||||||
this.end();
|
this.end();
|
||||||
return;
|
return;
|
||||||
|
@ -9,7 +9,9 @@ export abstract class PokemonPhase extends FieldPhase {
|
|||||||
* TODO: Make this either use IDs or `BattlerIndex`es, not a weird mix of both
|
* TODO: Make this either use IDs or `BattlerIndex`es, not a weird mix of both
|
||||||
*/
|
*/
|
||||||
protected battlerIndex: BattlerIndex | number;
|
protected battlerIndex: BattlerIndex | number;
|
||||||
|
// TODO: Why is this needed?
|
||||||
public player: boolean;
|
public player: boolean;
|
||||||
|
/** @todo Remove in favor of `battlerIndex` pleas for fuck's sake */
|
||||||
public fieldIndex: number;
|
public fieldIndex: number;
|
||||||
|
|
||||||
constructor(battlerIndex?: BattlerIndex | number) {
|
constructor(battlerIndex?: BattlerIndex | number) {
|
||||||
@ -32,10 +34,11 @@ export abstract class PokemonPhase extends FieldPhase {
|
|||||||
this.fieldIndex = battlerIndex % 2;
|
this.fieldIndex = battlerIndex % 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: This should have `undefined` in its signature
|
||||||
getPokemon(): Pokemon {
|
getPokemon(): Pokemon {
|
||||||
if (this.battlerIndex > BattlerIndex.ENEMY_2) {
|
if (this.battlerIndex > BattlerIndex.ENEMY_2) {
|
||||||
return globalScene.getPokemonById(this.battlerIndex)!; //TODO: is this bang correct?
|
return globalScene.getPokemonById(this.battlerIndex)!;
|
||||||
}
|
}
|
||||||
return globalScene.getField()[this.battlerIndex]!; //TODO: is this bang correct?
|
return globalScene.getField()[this.battlerIndex]!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import type { PlayerPokemon } from "#field/pokemon";
|
|||||||
import { BattlePhase } from "#phases/battle-phase";
|
import { BattlePhase } from "#phases/battle-phase";
|
||||||
import type { PartyOption } from "#ui/party-ui-handler";
|
import type { PartyOption } from "#ui/party-ui-handler";
|
||||||
import { PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler";
|
import { PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler";
|
||||||
import { isNullOrUndefined, toDmgValue } from "#utils/common";
|
import { toDmgValue } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,11 +42,7 @@ export class RevivalBlessingPhase extends BattlePhase {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const allyPokemon = this.user.getAlly();
|
const allyPokemon = this.user.getAlly();
|
||||||
if (
|
if (globalScene.currentBattle.double && globalScene.getPlayerParty().length > 1 && allyPokemon != null) {
|
||||||
globalScene.currentBattle.double
|
|
||||||
&& globalScene.getPlayerParty().length > 1
|
|
||||||
&& !isNullOrUndefined(allyPokemon)
|
|
||||||
) {
|
|
||||||
if (slotIndex <= 1) {
|
if (slotIndex <= 1) {
|
||||||
// Revived ally pokemon
|
// Revived ally pokemon
|
||||||
globalScene.phaseManager.unshiftNew(
|
globalScene.phaseManager.unshiftNew(
|
||||||
|
@ -27,7 +27,7 @@ import { BattlePhase } from "#phases/battle-phase";
|
|||||||
import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler";
|
import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler";
|
||||||
import { SHOP_OPTIONS_ROW_LIMIT } from "#ui/modifier-select-ui-handler";
|
import { SHOP_OPTIONS_ROW_LIMIT } from "#ui/modifier-select-ui-handler";
|
||||||
import { PartyOption, PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler";
|
import { PartyOption, PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler";
|
||||||
import { isNullOrUndefined, NumberHolder } from "#utils/common";
|
import { NumberHolder } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export type ModifierSelectCallback = (rowCursor: number, cursor: number) => boolean;
|
export type ModifierSelectCallback = (rowCursor: number, cursor: number) => boolean;
|
||||||
@ -429,7 +429,7 @@ export class SelectModifierPhase extends BattlePhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let multiplier = 1;
|
let multiplier = 1;
|
||||||
if (!isNullOrUndefined(this.customModifierSettings?.rerollMultiplier)) {
|
if (this.customModifierSettings?.rerollMultiplier != null) {
|
||||||
if (this.customModifierSettings.rerollMultiplier < 0) {
|
if (this.customModifierSettings.rerollMultiplier < 0) {
|
||||||
// Completely overrides reroll cost to -1 and early exits
|
// Completely overrides reroll cost to -1 and early exits
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -10,7 +10,6 @@ import { overrideHeldItems, overrideModifiers } from "#modifiers/modifier";
|
|||||||
import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler";
|
import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler";
|
||||||
import type { Starter } from "#ui/starter-select-ui-handler";
|
import type { Starter } from "#ui/starter-select-ui-handler";
|
||||||
import { applyChallenges } from "#utils/challenge-utils";
|
import { applyChallenges } from "#utils/challenge-utils";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
|
import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
|
||||||
|
|
||||||
@ -51,7 +50,7 @@ export class SelectStarterPhase extends Phase {
|
|||||||
let starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0));
|
let starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0));
|
||||||
if (
|
if (
|
||||||
starter.species.speciesId in Overrides.STARTER_FORM_OVERRIDES
|
starter.species.speciesId in Overrides.STARTER_FORM_OVERRIDES
|
||||||
&& !isNullOrUndefined(Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId])
|
&& Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId] != null
|
||||||
&& starter.species.forms[Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]!]
|
&& starter.species.forms[Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]!]
|
||||||
) {
|
) {
|
||||||
starterFormIndex = Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]!;
|
starterFormIndex = Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]!;
|
||||||
@ -89,7 +88,7 @@ export class SelectStarterPhase extends Phase {
|
|||||||
starterPokemon.nickname = starter.nickname;
|
starterPokemon.nickname = starter.nickname;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isNullOrUndefined(starter.teraType)) {
|
if (starter.teraType != null) {
|
||||||
starterPokemon.teraType = starter.teraType;
|
starterPokemon.teraType = starter.teraType;
|
||||||
} else {
|
} else {
|
||||||
starterPokemon.teraType = starterPokemon.species.type1;
|
starterPokemon.teraType = starterPokemon.species.type1;
|
||||||
|
@ -13,7 +13,7 @@ import type { Pokemon } from "#field/pokemon";
|
|||||||
import { ResetNegativeStatStageModifier } from "#modifiers/modifier";
|
import { ResetNegativeStatStageModifier } from "#modifiers/modifier";
|
||||||
import { PokemonPhase } from "#phases/pokemon-phase";
|
import { PokemonPhase } from "#phases/pokemon-phase";
|
||||||
import type { ConditionalUserFieldProtectStatAbAttrParams, PreStatStageChangeAbAttrParams } from "#types/ability-types";
|
import type { ConditionalUserFieldProtectStatAbAttrParams, PreStatStageChangeAbAttrParams } from "#types/ability-types";
|
||||||
import { BooleanHolder, isNullOrUndefined, NumberHolder } from "#utils/common";
|
import { BooleanHolder, NumberHolder } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export type StatStageChangeCallback = (
|
export type StatStageChangeCallback = (
|
||||||
@ -153,7 +153,7 @@ export class StatStageChangePhase extends PokemonPhase {
|
|||||||
applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", abAttrParams);
|
applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", abAttrParams);
|
||||||
// TODO: Consider skipping this call if `cancelled` is false.
|
// TODO: Consider skipping this call if `cancelled` is false.
|
||||||
const ally = pokemon.getAlly();
|
const ally = pokemon.getAlly();
|
||||||
if (!isNullOrUndefined(ally)) {
|
if (ally != null) {
|
||||||
applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", { ...abAttrParams, pokemon: ally });
|
applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", { ...abAttrParams, pokemon: ally });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import { vouchers } from "#system/voucher";
|
|||||||
import type { SessionSaveData } from "#types/save-data";
|
import type { SessionSaveData } from "#types/save-data";
|
||||||
import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
||||||
import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler";
|
import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler";
|
||||||
import { isLocal, isLocalServerConnected, isNullOrUndefined } from "#utils/common";
|
import { isLocal, isLocalServerConnected } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export class TitlePhase extends Phase {
|
export class TitlePhase extends Phase {
|
||||||
@ -289,7 +289,7 @@ export class TitlePhase extends Phase {
|
|||||||
} else {
|
} else {
|
||||||
// Grab first 10 chars of ISO date format (YYYY-MM-DD) and convert to base64
|
// Grab first 10 chars of ISO date format (YYYY-MM-DD) and convert to base64
|
||||||
let seed: string = btoa(new Date().toISOString().substring(0, 10));
|
let seed: string = btoa(new Date().toISOString().substring(0, 10));
|
||||||
if (!isNullOrUndefined(Overrides.DAILY_RUN_SEED_OVERRIDE)) {
|
if (Overrides.DAILY_RUN_SEED_OVERRIDE != null) {
|
||||||
seed = Overrides.DAILY_RUN_SEED_OVERRIDE;
|
seed = Overrides.DAILY_RUN_SEED_OVERRIDE;
|
||||||
}
|
}
|
||||||
generateDaily(seed);
|
generateDaily(seed);
|
||||||
|
@ -2,7 +2,6 @@ import { globalScene } from "#app/global-scene";
|
|||||||
import { VariantTier } from "#enums/variant-tier";
|
import { VariantTier } from "#enums/variant-tier";
|
||||||
import type { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import { hasExpSprite } from "#sprites/sprite-utils";
|
import { hasExpSprite } from "#sprites/sprite-utils";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
|
|
||||||
export type Variant = 0 | 1 | 2;
|
export type Variant = 0 | 1 | 2;
|
||||||
|
|
||||||
@ -138,7 +137,7 @@ export async function populateVariantColorCache(
|
|||||||
return fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error);
|
return fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error);
|
||||||
})
|
})
|
||||||
.then(c => {
|
.then(c => {
|
||||||
if (!isNullOrUndefined(c)) {
|
if (c != null) {
|
||||||
variantColorCache[cacheKey] = c;
|
variantColorCache[cacheKey] = c;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,6 @@ import { globalScene } from "#app/global-scene";
|
|||||||
import { pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
import { pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
||||||
import type { SpeciesId } from "#enums/species-id";
|
import type { SpeciesId } from "#enums/species-id";
|
||||||
import type { RibbonFlag } from "#system/ribbons/ribbon-data";
|
import type { RibbonFlag } from "#system/ribbons/ribbon-data";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Award one or more ribbons to a species and its pre-evolutions
|
* Award one or more ribbons to a species and its pre-evolutions
|
||||||
@ -14,7 +13,7 @@ export function awardRibbonsToSpeciesLine(id: SpeciesId, ribbons: RibbonFlag): v
|
|||||||
const dexData = globalScene.gameData.dexData;
|
const dexData = globalScene.gameData.dexData;
|
||||||
dexData[id].ribbons.award(ribbons);
|
dexData[id].ribbons.award(ribbons);
|
||||||
// Mark all pre-evolutions of the Pokémon with the same ribbon flags.
|
// Mark all pre-evolutions of the Pokémon with the same ribbon flags.
|
||||||
for (let prevoId = pokemonPrevolutions[id]; !isNullOrUndefined(prevoId); prevoId = pokemonPrevolutions[prevoId]) {
|
for (let prevoId = pokemonPrevolutions[id]; prevoId != null; prevoId = pokemonPrevolutions[prevoId]) {
|
||||||
dexData[prevoId].ribbons.award(ribbons);
|
dexData[prevoId].ribbons.award(ribbons);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import type { SessionSaveData, SystemSaveData } from "#types/save-data";
|
|||||||
import type { SessionSaveMigrator } from "#types/session-save-migrator";
|
import type { SessionSaveMigrator } from "#types/session-save-migrator";
|
||||||
import type { SettingsSaveMigrator } from "#types/settings-save-migrator";
|
import type { SettingsSaveMigrator } from "#types/settings-save-migrator";
|
||||||
import type { SystemSaveMigrator } from "#types/system-save-migrator";
|
import type { SystemSaveMigrator } from "#types/system-save-migrator";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate ability starter data if empty for caught species.
|
* Migrate ability starter data if empty for caught species.
|
||||||
@ -82,7 +81,7 @@ const fixLegendaryStats: SystemSaveMigrator = {
|
|||||||
const fixStarterData: SystemSaveMigrator = {
|
const fixStarterData: SystemSaveMigrator = {
|
||||||
version: "1.0.4",
|
version: "1.0.4",
|
||||||
migrate: (data: SystemSaveData): void => {
|
migrate: (data: SystemSaveData): void => {
|
||||||
if (!isNullOrUndefined(data.starterData)) {
|
if (data.starterData != null) {
|
||||||
for (const starterId of defaultStarterSpecies) {
|
for (const starterId of defaultStarterSpecies) {
|
||||||
if (data.starterData[starterId]?.abilityAttr) {
|
if (data.starterData[starterId]?.abilityAttr) {
|
||||||
data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1;
|
data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1;
|
||||||
@ -198,7 +197,7 @@ const migrateCustomPokemonData: SessionSaveMigrator = {
|
|||||||
pokemon["fusionMysteryEncounterPokemonData"] = null;
|
pokemon["fusionMysteryEncounterPokemonData"] = null;
|
||||||
}
|
}
|
||||||
pokemon.customPokemonData = pokemon.customPokemonData ?? new CustomPokemonData();
|
pokemon.customPokemonData = pokemon.customPokemonData ?? new CustomPokemonData();
|
||||||
if (!isNullOrUndefined(pokemon["natureOverride"]) && pokemon["natureOverride"] >= 0) {
|
if (pokemon["natureOverride"] != null && pokemon["natureOverride"] >= 0) {
|
||||||
pokemon.customPokemonData.nature = pokemon["natureOverride"];
|
pokemon.customPokemonData.nature = pokemon["natureOverride"];
|
||||||
pokemon["natureOverride"] = -1;
|
pokemon["natureOverride"] = -1;
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ import { DexAttr } from "#enums/dex-attr";
|
|||||||
import type { SessionSaveData, SystemSaveData } from "#types/save-data";
|
import type { SessionSaveData, SystemSaveData } from "#types/save-data";
|
||||||
import type { SessionSaveMigrator } from "#types/session-save-migrator";
|
import type { SessionSaveMigrator } from "#types/session-save-migrator";
|
||||||
import type { SystemSaveMigrator } from "#types/system-save-migrator";
|
import type { SystemSaveMigrator } from "#types/system-save-migrator";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -68,13 +67,13 @@ const migrateTera: SessionSaveMigrator = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data.party.forEach(p => {
|
data.party.forEach(p => {
|
||||||
if (isNullOrUndefined(p.teraType)) {
|
if (p.teraType == null) {
|
||||||
p.teraType = getPokemonSpeciesForm(p.species, p.formIndex).type1;
|
p.teraType = getPokemonSpeciesForm(p.species, p.formIndex).type1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
data.enemyParty.forEach(p => {
|
data.enemyParty.forEach(p => {
|
||||||
if (isNullOrUndefined(p.teraType)) {
|
if (p.teraType == null) {
|
||||||
p.teraType = getPokemonSpeciesForm(p.species, p.formIndex).type1;
|
p.teraType = getPokemonSpeciesForm(p.species, p.formIndex).type1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,6 @@ import { TextStyle } from "#enums/text-style";
|
|||||||
import { WeatherType } from "#enums/weather-type";
|
import { WeatherType } from "#enums/weather-type";
|
||||||
import { addTextObject } from "#ui/text";
|
import { addTextObject } from "#ui/text";
|
||||||
import type { nil } from "#utils/common";
|
import type { nil } from "#utils/common";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export enum EventType {
|
export enum EventType {
|
||||||
@ -428,7 +427,7 @@ export class TimedEventManager {
|
|||||||
|
|
||||||
getEventBannerLangs(): string[] {
|
getEventBannerLangs(): string[] {
|
||||||
const ret: string[] = [];
|
const ret: string[] = [];
|
||||||
ret.push(...timedEvents.find(te => this.isActive(te) && !isNullOrUndefined(te.availableLangs))?.availableLangs!);
|
ret.push(...timedEvents.find(te => this.isActive(te) && te.availableLangs != null)?.availableLangs!);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -437,7 +436,7 @@ export class TimedEventManager {
|
|||||||
timedEvents
|
timedEvents
|
||||||
.filter(te => this.isActive(te))
|
.filter(te => this.isActive(te))
|
||||||
.map(te => {
|
.map(te => {
|
||||||
if (!isNullOrUndefined(te.eventEncounters)) {
|
if (te.eventEncounters != null) {
|
||||||
ret.push(...te.eventEncounters);
|
ret.push(...te.eventEncounters);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -452,7 +451,7 @@ export class TimedEventManager {
|
|||||||
let multiplier = CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER;
|
let multiplier = CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER;
|
||||||
const classicFriendshipEvents = timedEvents.filter(te => this.isActive(te));
|
const classicFriendshipEvents = timedEvents.filter(te => this.isActive(te));
|
||||||
for (const fe of classicFriendshipEvents) {
|
for (const fe of classicFriendshipEvents) {
|
||||||
if (!isNullOrUndefined(fe.classicFriendshipMultiplier) && fe.classicFriendshipMultiplier > multiplier) {
|
if (fe.classicFriendshipMultiplier != null && fe.classicFriendshipMultiplier > multiplier) {
|
||||||
multiplier = fe.classicFriendshipMultiplier;
|
multiplier = fe.classicFriendshipMultiplier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -476,7 +475,7 @@ export class TimedEventManager {
|
|||||||
timedEvents
|
timedEvents
|
||||||
.filter(te => this.isActive(te))
|
.filter(te => this.isActive(te))
|
||||||
.map(te => {
|
.map(te => {
|
||||||
if (!isNullOrUndefined(te.delibirdyBuff)) {
|
if (te.delibirdyBuff != null) {
|
||||||
ret.push(...te.delibirdyBuff);
|
ret.push(...te.delibirdyBuff);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -492,7 +491,7 @@ export class TimedEventManager {
|
|||||||
timedEvents
|
timedEvents
|
||||||
.filter(te => this.isActive(te))
|
.filter(te => this.isActive(te))
|
||||||
.map(te => {
|
.map(te => {
|
||||||
if (!isNullOrUndefined(te.weather)) {
|
if (te.weather != null) {
|
||||||
ret.push(...te.weather);
|
ret.push(...te.weather);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -504,7 +503,7 @@ export class TimedEventManager {
|
|||||||
timedEvents
|
timedEvents
|
||||||
.filter(te => this.isActive(te))
|
.filter(te => this.isActive(te))
|
||||||
.map(te => {
|
.map(te => {
|
||||||
if (!isNullOrUndefined(te.mysteryEncounterTierChanges)) {
|
if (te.mysteryEncounterTierChanges != null) {
|
||||||
ret.push(...te.mysteryEncounterTierChanges);
|
ret.push(...te.mysteryEncounterTierChanges);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -514,7 +513,7 @@ export class TimedEventManager {
|
|||||||
getEventMysteryEncountersDisabled(): MysteryEncounterType[] {
|
getEventMysteryEncountersDisabled(): MysteryEncounterType[] {
|
||||||
const ret: MysteryEncounterType[] = [];
|
const ret: MysteryEncounterType[] = [];
|
||||||
timedEvents
|
timedEvents
|
||||||
.filter(te => this.isActive(te) && !isNullOrUndefined(te.mysteryEncounterTierChanges))
|
.filter(te => this.isActive(te) && te.mysteryEncounterTierChanges != null)
|
||||||
.map(te => {
|
.map(te => {
|
||||||
te.mysteryEncounterTierChanges?.map(metc => {
|
te.mysteryEncounterTierChanges?.map(metc => {
|
||||||
if (metc.disable) {
|
if (metc.disable) {
|
||||||
@ -531,7 +530,7 @@ export class TimedEventManager {
|
|||||||
): MysteryEncounterTier {
|
): MysteryEncounterTier {
|
||||||
let ret = normal;
|
let ret = normal;
|
||||||
timedEvents
|
timedEvents
|
||||||
.filter(te => this.isActive(te) && !isNullOrUndefined(te.mysteryEncounterTierChanges))
|
.filter(te => this.isActive(te) && te.mysteryEncounterTierChanges != null)
|
||||||
.map(te => {
|
.map(te => {
|
||||||
te.mysteryEncounterTierChanges?.map(metc => {
|
te.mysteryEncounterTierChanges?.map(metc => {
|
||||||
if (metc.mysteryEncounter === encounterType) {
|
if (metc.mysteryEncounter === encounterType) {
|
||||||
@ -544,7 +543,7 @@ export class TimedEventManager {
|
|||||||
|
|
||||||
getEventLuckBoost(): number {
|
getEventLuckBoost(): number {
|
||||||
let ret = 0;
|
let ret = 0;
|
||||||
const luckEvents = timedEvents.filter(te => this.isActive(te) && !isNullOrUndefined(te.luckBoost));
|
const luckEvents = timedEvents.filter(te => this.isActive(te) && te.luckBoost != null);
|
||||||
for (const le of luckEvents) {
|
for (const le of luckEvents) {
|
||||||
ret += le.luckBoost!;
|
ret += le.luckBoost!;
|
||||||
}
|
}
|
||||||
@ -556,7 +555,7 @@ export class TimedEventManager {
|
|||||||
timedEvents
|
timedEvents
|
||||||
.filter(te => this.isActive(te))
|
.filter(te => this.isActive(te))
|
||||||
.map(te => {
|
.map(te => {
|
||||||
if (!isNullOrUndefined(te.luckBoostedSpecies)) {
|
if (te.luckBoostedSpecies != null) {
|
||||||
ret.push(...te.luckBoostedSpecies.filter(s => !ret.includes(s)));
|
ret.push(...te.luckBoostedSpecies.filter(s => !ret.includes(s)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -576,7 +575,7 @@ export class TimedEventManager {
|
|||||||
getFixedBattleEventRewards(wave: number): string[] {
|
getFixedBattleEventRewards(wave: number): string[] {
|
||||||
const ret: string[] = [];
|
const ret: string[] = [];
|
||||||
timedEvents
|
timedEvents
|
||||||
.filter(te => this.isActive(te) && !isNullOrUndefined(te.classicWaveRewards))
|
.filter(te => this.isActive(te) && te.classicWaveRewards != null)
|
||||||
.map(te => {
|
.map(te => {
|
||||||
ret.push(...te.classicWaveRewards!.filter(cwr => cwr.wave === wave).map(cwr => cwr.type));
|
ret.push(...te.classicWaveRewards!.filter(cwr => cwr.wave === wave).map(cwr => cwr.type));
|
||||||
});
|
});
|
||||||
@ -586,7 +585,7 @@ export class TimedEventManager {
|
|||||||
// Gets the extra shiny chance for trainers due to event (odds/65536)
|
// Gets the extra shiny chance for trainers due to event (odds/65536)
|
||||||
getClassicTrainerShinyChance(): number {
|
getClassicTrainerShinyChance(): number {
|
||||||
let ret = 0;
|
let ret = 0;
|
||||||
const tsEvents = timedEvents.filter(te => this.isActive(te) && !isNullOrUndefined(te.trainerShinyChance));
|
const tsEvents = timedEvents.filter(te => this.isActive(te) && te.trainerShinyChance != null);
|
||||||
tsEvents.map(t => (ret += t.trainerShinyChance!));
|
tsEvents.map(t => (ret += t.trainerShinyChance!));
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@ -594,7 +593,7 @@ export class TimedEventManager {
|
|||||||
getEventBgmReplacement(bgm: string): string {
|
getEventBgmReplacement(bgm: string): string {
|
||||||
let ret = bgm;
|
let ret = bgm;
|
||||||
timedEvents.map(te => {
|
timedEvents.map(te => {
|
||||||
if (this.isActive(te) && !isNullOrUndefined(te.music)) {
|
if (this.isActive(te) && te.music != null) {
|
||||||
te.music.map(mr => {
|
te.music.map(mr => {
|
||||||
if (mr[0] === bgm) {
|
if (mr[0] === bgm) {
|
||||||
console.log(`it is ${te.name} so instead of ${mr[0]} we play ${mr[1]}`);
|
console.log(`it is ${te.name} so instead of ${mr[0]} we play ${mr[1]}`);
|
||||||
|
@ -3,7 +3,6 @@ import type { PokemonSpecies } from "#data/pokemon-species";
|
|||||||
import { TextStyle } from "#enums/text-style";
|
import { TextStyle } from "#enums/text-style";
|
||||||
import type { Variant } from "#sprites/variant";
|
import type { Variant } from "#sprites/variant";
|
||||||
import { addTextObject } from "#ui/text";
|
import { addTextObject } from "#ui/text";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
|
|
||||||
interface SpeciesDetails {
|
interface SpeciesDetails {
|
||||||
shiny?: boolean;
|
shiny?: boolean;
|
||||||
@ -177,16 +176,16 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container {
|
|||||||
const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, false, true);
|
const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, false, true);
|
||||||
const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
|
const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
|
||||||
|
|
||||||
if (!isNullOrUndefined(formIndex)) {
|
if (formIndex != null) {
|
||||||
defaultProps.formIndex = formIndex;
|
defaultProps.formIndex = formIndex;
|
||||||
}
|
}
|
||||||
if (!isNullOrUndefined(shiny)) {
|
if (shiny != null) {
|
||||||
defaultProps.shiny = shiny;
|
defaultProps.shiny = shiny;
|
||||||
}
|
}
|
||||||
if (!isNullOrUndefined(variant)) {
|
if (variant != null) {
|
||||||
defaultProps.variant = variant;
|
defaultProps.variant = variant;
|
||||||
}
|
}
|
||||||
if (!isNullOrUndefined(female)) {
|
if (female != null) {
|
||||||
defaultProps.female = female;
|
defaultProps.female = female;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import { PartyUiMode } from "#ui/party-ui-handler";
|
|||||||
import { addBBCodeTextObject, getBBCodeFrag } from "#ui/text";
|
import { addBBCodeTextObject, getBBCodeFrag } from "#ui/text";
|
||||||
import { UiHandler } from "#ui/ui-handler";
|
import { UiHandler } from "#ui/ui-handler";
|
||||||
import { addWindow, WindowVariant } from "#ui/ui-theme";
|
import { addWindow, WindowVariant } from "#ui/ui-theme";
|
||||||
import { fixedInt, isNullOrUndefined } from "#utils/common";
|
import { fixedInt } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
|
import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
|
||||||
|
|
||||||
@ -95,12 +95,10 @@ export class MysteryEncounterUiHandler extends UiHandler {
|
|||||||
super.show(args);
|
super.show(args);
|
||||||
|
|
||||||
this.overrideSettings = (args[0] as OptionSelectSettings) ?? {};
|
this.overrideSettings = (args[0] as OptionSelectSettings) ?? {};
|
||||||
const showDescriptionContainer = isNullOrUndefined(this.overrideSettings?.hideDescription)
|
const showDescriptionContainer =
|
||||||
? true
|
this.overrideSettings?.hideDescription == null ? true : !this.overrideSettings.hideDescription;
|
||||||
: !this.overrideSettings.hideDescription;
|
const slideInDescription =
|
||||||
const slideInDescription = isNullOrUndefined(this.overrideSettings?.slideInDescription)
|
this.overrideSettings?.slideInDescription == null ? true : this.overrideSettings.slideInDescription;
|
||||||
? true
|
|
||||||
: this.overrideSettings.slideInDescription;
|
|
||||||
const startingCursorIndex = this.overrideSettings?.startingCursorIndex ?? 0;
|
const startingCursorIndex = this.overrideSettings?.startingCursorIndex ?? 0;
|
||||||
|
|
||||||
this.cursorContainer.setVisible(true);
|
this.cursorContainer.setVisible(true);
|
||||||
@ -567,7 +565,7 @@ export class MysteryEncounterUiHandler extends UiHandler {
|
|||||||
}
|
}
|
||||||
this.tooltipContainer.setVisible(true);
|
this.tooltipContainer.setVisible(true);
|
||||||
|
|
||||||
if (isNullOrUndefined(cursor) || cursor > this.optionsContainer.length - 2) {
|
if (cursor == null || cursor > this.optionsContainer.length - 2) {
|
||||||
// Ignore hovers on view party button
|
// Ignore hovers on view party button
|
||||||
// Hide dex progress if visible
|
// Hide dex progress if visible
|
||||||
this.showHideDexProgress(false);
|
this.showHideDexProgress(false);
|
||||||
|
@ -54,7 +54,7 @@ import { PokedexInfoOverlay } from "#ui/pokedex-info-overlay";
|
|||||||
import { StatsContainer } from "#ui/stats-container";
|
import { StatsContainer } from "#ui/stats-container";
|
||||||
import { addBBCodeTextObject, addTextObject, getTextColor, getTextStyleOptions } from "#ui/text";
|
import { addBBCodeTextObject, addTextObject, getTextColor, getTextStyleOptions } from "#ui/text";
|
||||||
import { addWindow } from "#ui/ui-theme";
|
import { addWindow } from "#ui/ui-theme";
|
||||||
import { BooleanHolder, getLocalizedSpriteKey, isNullOrUndefined, padInt, rgbHexToRgba } from "#utils/common";
|
import { BooleanHolder, getLocalizedSpriteKey, padInt, rgbHexToRgba } from "#utils/common";
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||||
import { toCamelCase, toTitleCase } from "#utils/strings";
|
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||||
@ -2424,11 +2424,7 @@ export class PokedexPageUiHandler extends MessageUiHandler {
|
|||||||
// We will only update the sprite if there is a change to form, shiny/variant
|
// We will only update the sprite if there is a change to form, shiny/variant
|
||||||
// or gender for species with gender sprite differences
|
// or gender for species with gender sprite differences
|
||||||
const shouldUpdateSprite =
|
const shouldUpdateSprite =
|
||||||
(species?.genderDiffs && !isNullOrUndefined(female))
|
(species?.genderDiffs && female != null) || formIndex != null || shiny != null || variant != null || forceUpdate;
|
||||||
|| !isNullOrUndefined(formIndex)
|
|
||||||
|| !isNullOrUndefined(shiny)
|
|
||||||
|| !isNullOrUndefined(variant)
|
|
||||||
|| forceUpdate;
|
|
||||||
|
|
||||||
if (this.activeTooltip === "CANDY") {
|
if (this.activeTooltip === "CANDY") {
|
||||||
if (this.species && this.pokemonCandyContainer.visible) {
|
if (this.species && this.pokemonCandyContainer.visible) {
|
||||||
|
@ -6,7 +6,6 @@ import { FilterTextRow } from "#ui/filter-text";
|
|||||||
import type { InputFieldConfig } from "#ui/form-modal-ui-handler";
|
import type { InputFieldConfig } from "#ui/form-modal-ui-handler";
|
||||||
import { FormModalUiHandler } from "#ui/form-modal-ui-handler";
|
import { FormModalUiHandler } from "#ui/form-modal-ui-handler";
|
||||||
import type { ModalConfig } from "#ui/modal-ui-handler";
|
import type { ModalConfig } from "#ui/modal-ui-handler";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export class PokedexScanUiHandler extends FormModalUiHandler {
|
export class PokedexScanUiHandler extends FormModalUiHandler {
|
||||||
@ -132,7 +131,7 @@ export class PokedexScanUiHandler extends FormModalUiHandler {
|
|||||||
return {
|
return {
|
||||||
label: value,
|
label: value,
|
||||||
handler: () => {
|
handler: () => {
|
||||||
if (!isNullOrUndefined(evt.data) || evt.inputType?.toLowerCase() === "deletecontentbackward") {
|
if (evt.data != null || evt.inputType?.toLowerCase() === "deletecontentbackward") {
|
||||||
inputObject.setText(value);
|
inputObject.setText(value);
|
||||||
}
|
}
|
||||||
ui.revertMode();
|
ui.revertMode();
|
||||||
|
@ -13,7 +13,7 @@ import { MessageUiHandler } from "#ui/message-ui-handler";
|
|||||||
import { RunDisplayMode } from "#ui/run-info-ui-handler";
|
import { RunDisplayMode } from "#ui/run-info-ui-handler";
|
||||||
import { addTextObject } from "#ui/text";
|
import { addTextObject } from "#ui/text";
|
||||||
import { addWindow } from "#ui/ui-theme";
|
import { addWindow } from "#ui/ui-theme";
|
||||||
import { fixedInt, formatLargeNumber, getPlayTimeString, isNullOrUndefined } from "#utils/common";
|
import { fixedInt, formatLargeNumber, getPlayTimeString } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
const SESSION_SLOTS_COUNT = 5;
|
const SESSION_SLOTS_COUNT = 5;
|
||||||
@ -405,7 +405,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
}
|
}
|
||||||
this.setArrowVisibility(hasData);
|
this.setArrowVisibility(hasData);
|
||||||
}
|
}
|
||||||
if (!isNullOrUndefined(prevSlotIndex)) {
|
if (prevSlotIndex != null) {
|
||||||
this.revertSessionSlot(prevSlotIndex);
|
this.revertSessionSlot(prevSlotIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,6 @@ import {
|
|||||||
BooleanHolder,
|
BooleanHolder,
|
||||||
fixedInt,
|
fixedInt,
|
||||||
getLocalizedSpriteKey,
|
getLocalizedSpriteKey,
|
||||||
isNullOrUndefined,
|
|
||||||
NumberHolder,
|
NumberHolder,
|
||||||
padInt,
|
padInt,
|
||||||
randIntRange,
|
randIntRange,
|
||||||
@ -2548,7 +2547,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
case Button.CYCLE_TERA:
|
case Button.CYCLE_TERA:
|
||||||
if (this.canCycleTera) {
|
if (this.canCycleTera) {
|
||||||
const speciesForm = getPokemonSpeciesForm(this.lastSpecies.speciesId, starterAttributes.form ?? 0);
|
const speciesForm = getPokemonSpeciesForm(this.lastSpecies.speciesId, starterAttributes.form ?? 0);
|
||||||
if (speciesForm.type1 === this.teraCursor && !isNullOrUndefined(speciesForm.type2)) {
|
if (speciesForm.type1 === this.teraCursor && speciesForm.type2 != null) {
|
||||||
starterAttributes.tera = speciesForm.type2;
|
starterAttributes.tera = speciesForm.type2;
|
||||||
originalStarterAttributes.tera = starterAttributes.tera;
|
originalStarterAttributes.tera = starterAttributes.tera;
|
||||||
this.setSpeciesDetails(this.lastSpecies, {
|
this.setSpeciesDetails(this.lastSpecies, {
|
||||||
@ -2790,7 +2789,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
*/
|
*/
|
||||||
switchMoveHandler(targetIndex: number, newMove: MoveId, previousMove: MoveId) {
|
switchMoveHandler(targetIndex: number, newMove: MoveId, previousMove: MoveId) {
|
||||||
const starterMoveset = this.starterMoveset;
|
const starterMoveset = this.starterMoveset;
|
||||||
if (isNullOrUndefined(starterMoveset)) {
|
if (starterMoveset == null) {
|
||||||
console.warn("Trying to update a non-existing moveset");
|
console.warn("Trying to update a non-existing moveset");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -3687,7 +3686,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isNullOrUndefined(props.formIndex)) {
|
if (props.formIndex != null) {
|
||||||
// If switching forms while the pokemon is in the team, update its moveset
|
// If switching forms while the pokemon is in the team, update its moveset
|
||||||
this.updateSelectedStarterMoveset(species.speciesId);
|
this.updateSelectedStarterMoveset(species.speciesId);
|
||||||
}
|
}
|
||||||
@ -3809,10 +3808,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
// We will only update the sprite if there is a change to form, shiny/variant
|
// We will only update the sprite if there is a change to form, shiny/variant
|
||||||
// or gender for species with gender sprite differences
|
// or gender for species with gender sprite differences
|
||||||
const shouldUpdateSprite =
|
const shouldUpdateSprite =
|
||||||
(species?.genderDiffs && !isNullOrUndefined(female))
|
(species?.genderDiffs && female != null) || formIndex != null || shiny != null || variant != null;
|
||||||
|| !isNullOrUndefined(formIndex)
|
|
||||||
|| !isNullOrUndefined(shiny)
|
|
||||||
|| !isNullOrUndefined(variant);
|
|
||||||
|
|
||||||
const isFreshStartChallenge = globalScene.gameMode.hasChallenge(Challenges.FRESH_START);
|
const isFreshStartChallenge = globalScene.gameMode.hasChallenge(Challenges.FRESH_START);
|
||||||
|
|
||||||
@ -3850,7 +3846,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
); // TODO: is this bang correct?
|
); // TODO: is this bang correct?
|
||||||
this.abilityCursor = abilityIndex !== undefined ? abilityIndex : (abilityIndex = oldAbilityIndex);
|
this.abilityCursor = abilityIndex !== undefined ? abilityIndex : (abilityIndex = oldAbilityIndex);
|
||||||
this.natureCursor = natureIndex !== undefined ? natureIndex : (natureIndex = oldNatureIndex);
|
this.natureCursor = natureIndex !== undefined ? natureIndex : (natureIndex = oldNatureIndex);
|
||||||
this.teraCursor = !isNullOrUndefined(teraType) ? teraType : (teraType = oldTeraType);
|
this.teraCursor = teraType != null ? teraType : (teraType = oldTeraType);
|
||||||
const [isInParty, partyIndex]: [boolean, number] = this.isInParty(species); // we use this to firstly check if the pokemon is in the party, and if so, to get the party index in order to update the icon image
|
const [isInParty, partyIndex]: [boolean, number] = this.isInParty(species); // we use this to firstly check if the pokemon is in the party, and if so, to get the party index in order to update the icon image
|
||||||
if (isInParty) {
|
if (isInParty) {
|
||||||
this.updatePartyIcon(species, partyIndex);
|
this.updatePartyIcon(species, partyIndex);
|
||||||
@ -3991,7 +3987,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
this.canCycleTera =
|
this.canCycleTera =
|
||||||
!this.statsMode
|
!this.statsMode
|
||||||
&& this.allowTera
|
&& this.allowTera
|
||||||
&& !isNullOrUndefined(getPokemonSpeciesForm(species.speciesId, formIndex ?? 0).type2)
|
&& getPokemonSpeciesForm(species.speciesId, formIndex ?? 0).type2 != null
|
||||||
&& !globalScene.gameMode.hasChallenge(Challenges.FRESH_START);
|
&& !globalScene.gameMode.hasChallenge(Challenges.FRESH_START);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4592,7 +4588,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
this.canCycleTera =
|
this.canCycleTera =
|
||||||
!this.statsMode
|
!this.statsMode
|
||||||
&& this.allowTera
|
&& this.allowTera
|
||||||
&& !isNullOrUndefined(getPokemonSpeciesForm(this.lastSpecies.speciesId, formIndex ?? 0).type2)
|
&& getPokemonSpeciesForm(this.lastSpecies.speciesId, formIndex ?? 0).type2 != null
|
||||||
&& !globalScene.gameMode.hasChallenge(Challenges.FRESH_START);
|
&& !globalScene.gameMode.hasChallenge(Challenges.FRESH_START);
|
||||||
this.updateInstructions();
|
this.updateInstructions();
|
||||||
}
|
}
|
||||||
|
@ -27,15 +27,7 @@ import { getVariantTint } from "#sprites/variant";
|
|||||||
import { achvs } from "#system/achv";
|
import { achvs } from "#system/achv";
|
||||||
import { addBBCodeTextObject, addTextObject, getBBCodeFrag, getTextColor } from "#ui/text";
|
import { addBBCodeTextObject, addTextObject, getBBCodeFrag, getTextColor } from "#ui/text";
|
||||||
import { UiHandler } from "#ui/ui-handler";
|
import { UiHandler } from "#ui/ui-handler";
|
||||||
import {
|
import { fixedInt, formatStat, getLocalizedSpriteKey, getShinyDescriptor, padInt, rgbHexToRgba } from "#utils/common";
|
||||||
fixedInt,
|
|
||||||
formatStat,
|
|
||||||
getLocalizedSpriteKey,
|
|
||||||
getShinyDescriptor,
|
|
||||||
isNullOrUndefined,
|
|
||||||
padInt,
|
|
||||||
rgbHexToRgba,
|
|
||||||
} from "#utils/common";
|
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
import { toCamelCase, toTitleCase } from "#utils/strings";
|
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||||
import { argbFromRgba } from "@material/material-color-utilities";
|
import { argbFromRgba } from "@material/material-color-utilities";
|
||||||
@ -895,10 +887,7 @@ export class SummaryUiHandler extends UiHandler {
|
|||||||
profileContainer.add(luckText);
|
profileContainer.add(luckText);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id) && this.pokemon != null) {
|
||||||
globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id)
|
|
||||||
&& !isNullOrUndefined(this.pokemon)
|
|
||||||
) {
|
|
||||||
const teraIcon = globalScene.add.sprite(123, 26, "button_tera");
|
const teraIcon = globalScene.add.sprite(123, 26, "button_tera");
|
||||||
teraIcon.setName("terastallize-icon");
|
teraIcon.setName("terastallize-icon");
|
||||||
teraIcon.setFrame(PokemonType[this.pokemon.getTeraType()].toLowerCase());
|
teraIcon.setFrame(PokemonType[this.pokemon.getTeraType()].toLowerCase());
|
||||||
|
@ -8,7 +8,7 @@ import type { Pokemon } from "#field/pokemon";
|
|||||||
import type { ModifierBar } from "#modifiers/modifier";
|
import type { ModifierBar } from "#modifiers/modifier";
|
||||||
import { getMoveTargets } from "#moves/move-utils";
|
import { getMoveTargets } from "#moves/move-utils";
|
||||||
import { UiHandler } from "#ui/ui-handler";
|
import { UiHandler } from "#ui/ui-handler";
|
||||||
import { fixedInt, isNullOrUndefined } from "#utils/common";
|
import { fixedInt } from "#utils/common";
|
||||||
|
|
||||||
export type TargetSelectCallback = (targets: BattlerIndex[]) => void;
|
export type TargetSelectCallback = (targets: BattlerIndex[]) => void;
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ export class TargetSelectUiHandler extends UiHandler {
|
|||||||
*/
|
*/
|
||||||
resetCursor(cursorN: number, user: Pokemon): void {
|
resetCursor(cursorN: number, user: Pokemon): void {
|
||||||
if (
|
if (
|
||||||
!isNullOrUndefined(cursorN)
|
cursorN != null
|
||||||
&& ([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2].includes(cursorN) || user.tempSummonData.waveTurnCount === 1)
|
&& ([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2].includes(cursorN) || user.tempSummonData.waveTurnCount === 1)
|
||||||
) {
|
) {
|
||||||
// Reset cursor on the first turn of a fight or if an ally was targeted last turn
|
// Reset cursor on the first turn of a fight or if an ally was targeted last turn
|
||||||
@ -90,13 +90,10 @@ export class TargetSelectUiHandler extends UiHandler {
|
|||||||
this.targetSelectCallback(button === Button.ACTION ? targetIndexes : []);
|
this.targetSelectCallback(button === Button.ACTION ? targetIndexes : []);
|
||||||
success = true;
|
success = true;
|
||||||
if (this.fieldIndex === BattlerIndex.PLAYER) {
|
if (this.fieldIndex === BattlerIndex.PLAYER) {
|
||||||
if (isNullOrUndefined(this.cursor0) || this.cursor0 !== this.cursor) {
|
if (this.cursor0 == null || this.cursor0 !== this.cursor) {
|
||||||
this.cursor0 = this.cursor;
|
this.cursor0 = this.cursor;
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (this.fieldIndex === BattlerIndex.PLAYER_2 && (this.cursor1 == null || this.cursor1 !== this.cursor)) {
|
||||||
this.fieldIndex === BattlerIndex.PLAYER_2
|
|
||||||
&& (isNullOrUndefined(this.cursor1) || this.cursor1 !== this.cursor)
|
|
||||||
) {
|
|
||||||
this.cursor1 = this.cursor;
|
this.cursor1 = this.cursor;
|
||||||
}
|
}
|
||||||
} else if (this.isMultipleTargets) {
|
} else if (this.isMultipleTargets) {
|
||||||
|
@ -4,7 +4,6 @@ import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
|||||||
import type { InputFieldConfig } from "#ui/form-modal-ui-handler";
|
import type { InputFieldConfig } from "#ui/form-modal-ui-handler";
|
||||||
import { FormModalUiHandler } from "#ui/form-modal-ui-handler";
|
import { FormModalUiHandler } from "#ui/form-modal-ui-handler";
|
||||||
import type { ModalConfig } from "#ui/modal-ui-handler";
|
import type { ModalConfig } from "#ui/modal-ui-handler";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export class TestDialogueUiHandler extends FormModalUiHandler {
|
export class TestDialogueUiHandler extends FormModalUiHandler {
|
||||||
@ -18,7 +17,7 @@ export class TestDialogueUiHandler extends FormModalUiHandler {
|
|||||||
.map((t, i) => {
|
.map((t, i) => {
|
||||||
const value = Object.values(object)[i];
|
const value = Object.values(object)[i];
|
||||||
|
|
||||||
if (typeof value === "object" && !isNullOrUndefined(value)) {
|
if (typeof value === "object" && value != null) {
|
||||||
// we check for not null or undefined here because if the language json file has a null key, the typeof will still be an object, but that object will be null, causing issues
|
// we check for not null or undefined here because if the language json file has a null key, the typeof will still be an object, but that object will be null, causing issues
|
||||||
// If the value is an object, execute the same process
|
// If the value is an object, execute the same process
|
||||||
// si el valor es un objeto ejecuta el mismo proceso
|
// si el valor es un objeto ejecuta el mismo proceso
|
||||||
@ -27,7 +26,7 @@ export class TestDialogueUiHandler extends FormModalUiHandler {
|
|||||||
t => t.length > 0,
|
t => t.length > 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (typeof value === "string" || isNullOrUndefined(value)) {
|
if (typeof value === "string" || value == null) {
|
||||||
// we check for null or undefined here as per above - the typeof is still an object but the value is null so we need to exit out of this and pass the null key
|
// we check for null or undefined here as per above - the typeof is still an object but the value is null so we need to exit out of this and pass the null key
|
||||||
|
|
||||||
// Return in the format expected by i18next
|
// Return in the format expected by i18next
|
||||||
@ -109,7 +108,7 @@ export class TestDialogueUiHandler extends FormModalUiHandler {
|
|||||||
handler: () => {
|
handler: () => {
|
||||||
// this is here to make sure that if you try to backspace then enter, the last known evt.data (backspace) is picked up
|
// this is here to make sure that if you try to backspace then enter, the last known evt.data (backspace) is picked up
|
||||||
// this is because evt.data is null for backspace, so without this, the autocomplete windows just closes
|
// this is because evt.data is null for backspace, so without this, the autocomplete windows just closes
|
||||||
if (!isNullOrUndefined(evt.data) || evt.inputType?.toLowerCase() === "deletecontentbackward") {
|
if (evt.data != null || evt.inputType?.toLowerCase() === "deletecontentbackward") {
|
||||||
const separatedArray = inputObject.text.split(" ");
|
const separatedArray = inputObject.text.split(" ");
|
||||||
separatedArray[separatedArray.length - 1] = value;
|
separatedArray[separatedArray.length - 1] = value;
|
||||||
inputObject.setText(separatedArray.join(" "));
|
inputObject.setText(separatedArray.join(" "));
|
||||||
|
26
src/utils/anim-utils.ts
Normal file
26
src/utils/anim-utils.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import type { SceneBase } from "#app/scene-base";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays a Tween animation, resolving once the animation completes.
|
||||||
|
* @param config - The config for a single Tween
|
||||||
|
* @param scene - The {@linkcode SceneBase} on which the Tween plays; default {@linkcode globalScene}
|
||||||
|
* @returns A Promise that resolves once the Tween has been played.
|
||||||
|
*
|
||||||
|
* @privateRemarks
|
||||||
|
* The `config` input should not include an `onComplete` field as that callback is
|
||||||
|
* used to resolve the Promise containing the Tween animation.
|
||||||
|
* However, `config`'s type cannot be changed to something like `Omit<TweenBuilderConfig, "onComplete">`
|
||||||
|
* due to how the type for `TweenBuilderConfig` is defined.
|
||||||
|
*/
|
||||||
|
export async function playTween(
|
||||||
|
config: Phaser.Types.Tweens.TweenBuilderConfig,
|
||||||
|
scene: SceneBase = globalScene,
|
||||||
|
): Promise<void> {
|
||||||
|
await new Promise(resolve =>
|
||||||
|
scene.tweens.add({
|
||||||
|
...config,
|
||||||
|
onComplete: resolve,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
@ -458,15 +458,6 @@ export function truncateString(str: string, maxLength = 10) {
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Report whether a given value is nullish (`null`/`undefined`).
|
|
||||||
* @param val - The value whose nullishness is being checked
|
|
||||||
* @returns `true` if `val` is either `null` or `undefined`
|
|
||||||
*/
|
|
||||||
export function isNullOrUndefined(val: any): val is null | undefined {
|
|
||||||
return val === null || val === undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is used in the context of a Pokémon battle game to calculate the actual integer damage value from a float result.
|
* This function is used in the context of a Pokémon battle game to calculate the actual integer damage value from a float result.
|
||||||
* Many damage calculation formulas involve various parameters and result in float values.
|
* Many damage calculation formulas involve various parameters and result in float values.
|
||||||
|
@ -23,11 +23,13 @@ export function getCookie(cName: string): string {
|
|||||||
}
|
}
|
||||||
const name = `${cName}=`;
|
const name = `${cName}=`;
|
||||||
const ca = document.cookie.split(";");
|
const ca = document.cookie.split(";");
|
||||||
// Check all cookies in the document and see if any of them match, grabbing the first one whose value lines up
|
for (let c of ca) {
|
||||||
for (const c of ca) {
|
// ⚠️ DO NOT REPLACE THIS WITH C = C.TRIM() - IT BREAKS IN NON-CHROMIUM BROWSERS ⚠️
|
||||||
const cTrimmed = c.trim();
|
while (c.charAt(0) === " ") {
|
||||||
if (cTrimmed.startsWith(name)) {
|
c = c.substring(1);
|
||||||
return c.slice(name.length, c.length);
|
}
|
||||||
|
if (c.indexOf(name) === 0) {
|
||||||
|
return c.substring(name.length, c.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
|
3
src/vite.env.d.ts
vendored
3
src/vite.env.d.ts
vendored
@ -9,8 +9,9 @@ interface ImportMetaEnv {
|
|||||||
readonly VITE_DISCORD_CLIENT_ID?: string;
|
readonly VITE_DISCORD_CLIENT_ID?: string;
|
||||||
readonly VITE_GOOGLE_CLIENT_ID?: string;
|
readonly VITE_GOOGLE_CLIENT_ID?: string;
|
||||||
readonly VITE_I18N_DEBUG?: string;
|
readonly VITE_I18N_DEBUG?: string;
|
||||||
|
readonly NODE_ENV?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
declare interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv;
|
readonly env: ImportMetaEnv;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import { SpeciesId } from "#enums/species-id";
|
|||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
import type { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
@ -37,7 +36,7 @@ describe("Abilities - Healer", () => {
|
|||||||
|
|
||||||
// Mock healer to have a 100% chance of healing its ally
|
// Mock healer to have a 100% chance of healing its ally
|
||||||
vi.spyOn(allAbilities[AbilityId.HEALER].getAttrs("PostTurnResetStatusAbAttr")[0], "getCondition").mockReturnValue(
|
vi.spyOn(allAbilities[AbilityId.HEALER].getAttrs("PostTurnResetStatusAbAttr")[0], "getCondition").mockReturnValue(
|
||||||
(pokemon: Pokemon) => !isNullOrUndefined(pokemon.getAlly()),
|
(pokemon: Pokemon) => pokemon.getAlly() != null,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
285
test/ai/ai-moveset-gen.test.ts
Normal file
285
test/ai/ai-moveset-gen.test.ts
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
import { __INTERNAL_TEST_EXPORTS } from "#app/ai/ai-moveset-gen";
|
||||||
|
import {
|
||||||
|
COMMON_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
GREAT_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
ULTRA_TIER_TM_LEVEL_REQUIREMENT,
|
||||||
|
} from "#balance/moveset-generation";
|
||||||
|
import { allMoves, allSpecies } from "#data/data-lists";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { TrainerSlot } from "#enums/trainer-slot";
|
||||||
|
import { EnemyPokemon } from "#field/pokemon";
|
||||||
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import { NumberHolder } from "#utils/common";
|
||||||
|
import { afterEach } from "node:test";
|
||||||
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for {@linkcode createTestablePokemon}
|
||||||
|
*/
|
||||||
|
interface MockPokemonParams {
|
||||||
|
/** The level to set the Pokémon to */
|
||||||
|
level: number;
|
||||||
|
/**
|
||||||
|
* Whether the pokemon is a boss or not.
|
||||||
|
* @defaultValue `false`
|
||||||
|
*/
|
||||||
|
boss?: boolean;
|
||||||
|
/**
|
||||||
|
* The trainer slot to assign to the pokemon, if any.
|
||||||
|
* @defaultValue `TrainerSlot.NONE`
|
||||||
|
*/
|
||||||
|
trainerSlot?: TrainerSlot;
|
||||||
|
/**
|
||||||
|
* The form index to assign to the pokemon, if any.
|
||||||
|
* This *must* be one of the valid form indices for the species, or the test will break.
|
||||||
|
* @defaultValue `0`
|
||||||
|
*/
|
||||||
|
formIndex?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an `EnemyPokemon` that can be used for testing
|
||||||
|
* @param species - The species ID of the pokemon to create
|
||||||
|
* @returns The newly created `EnemyPokemon`.
|
||||||
|
* @todo Move this to a dedicated unit test util folder if more tests come to rely on it
|
||||||
|
*/
|
||||||
|
function createTestablePokemon(
|
||||||
|
species: SpeciesId,
|
||||||
|
{ level, trainerSlot = TrainerSlot.NONE, boss = false, formIndex = 0 }: MockPokemonParams,
|
||||||
|
): EnemyPokemon {
|
||||||
|
const pokemon = new EnemyPokemon(allSpecies[species], level, trainerSlot, boss);
|
||||||
|
if (formIndex !== 0) {
|
||||||
|
const formIndexLength = allSpecies[species]?.forms.length;
|
||||||
|
const name = allSpecies[species]?.name;
|
||||||
|
expect(formIndex, `${name} does not have a form with index ${formIndex}`).toBeLessThan(formIndexLength);
|
||||||
|
pokemon.formIndex = formIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pokemon;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Unit Tests - ai-moveset-gen.ts", () => {
|
||||||
|
describe("filterPool", () => {
|
||||||
|
const { filterPool } = __INTERNAL_TEST_EXPORTS;
|
||||||
|
it("clones a pool when there are no predicates", () => {
|
||||||
|
const pool = new Map<MoveId, number>([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const filtered = filterPool(pool, () => true);
|
||||||
|
const expected = [
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
];
|
||||||
|
expect(filtered).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not modify the original pool", () => {
|
||||||
|
const pool = new Map<MoveId, number>([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
]);
|
||||||
|
const original = new Map(pool);
|
||||||
|
|
||||||
|
filterPool(pool, moveId => moveId !== MoveId.TACKLE);
|
||||||
|
expect(pool).toEqual(original);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters out moves that do not match the predicate", () => {
|
||||||
|
const pool = new Map<MoveId, number>([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
[MoveId.SPLASH, 3],
|
||||||
|
]);
|
||||||
|
const filtered = filterPool(pool, moveId => moveId !== MoveId.SPLASH);
|
||||||
|
expect(filtered).toEqual([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty array if no moves match the predicate", () => {
|
||||||
|
const pool = new Map<MoveId, number>([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
]);
|
||||||
|
const filtered = filterPool(pool, () => false);
|
||||||
|
expect(filtered).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calculates totalWeight correctly when provided", () => {
|
||||||
|
const pool = new Map<MoveId, number>([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
[MoveId.SPLASH, 3],
|
||||||
|
]);
|
||||||
|
const totalWeight = new NumberHolder(0);
|
||||||
|
const filtered = filterPool(pool, moveId => moveId !== MoveId.SPLASH, totalWeight);
|
||||||
|
expect(filtered).toEqual([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
]);
|
||||||
|
expect(totalWeight.value).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Clears totalWeight when provided", () => {
|
||||||
|
const pool = new Map<MoveId, number>([
|
||||||
|
[MoveId.TACKLE, 1],
|
||||||
|
[MoveId.FLAMETHROWER, 2],
|
||||||
|
]);
|
||||||
|
const totalWeight = new NumberHolder(42);
|
||||||
|
const filtered = filterPool(pool, () => false, totalWeight);
|
||||||
|
expect(filtered).toEqual([]);
|
||||||
|
expect(totalWeight.value).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAllowedTmTiers", () => {
|
||||||
|
const { getAllowedTmTiers } = __INTERNAL_TEST_EXPORTS;
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ tierName: "common", resIdx: 0, level: COMMON_TIER_TM_LEVEL_REQUIREMENT - 1 },
|
||||||
|
{ tierName: "great", resIdx: 1, level: GREAT_TIER_TM_LEVEL_REQUIREMENT - 1 },
|
||||||
|
{ tierName: "ultra", resIdx: 2, level: ULTRA_TIER_TM_LEVEL_REQUIREMENT - 1 },
|
||||||
|
])("should prevent $name TMs when below level $level", ({ level, resIdx }) => {
|
||||||
|
expect(getAllowedTmTiers(level)[resIdx]).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ tierName: "common", resIdx: 0, level: COMMON_TIER_TM_LEVEL_REQUIREMENT },
|
||||||
|
{ tierName: "great", resIdx: 1, level: GREAT_TIER_TM_LEVEL_REQUIREMENT },
|
||||||
|
{ tierName: "ultra", resIdx: 2, level: ULTRA_TIER_TM_LEVEL_REQUIREMENT },
|
||||||
|
])("should allow $name TMs when at level $level", ({ level, resIdx }) => {
|
||||||
|
expect(getAllowedTmTiers(level)[resIdx]).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unit tests for methods that require a game context
|
||||||
|
describe("", () => {
|
||||||
|
//#region boilerplate
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
/**A pokemon object that will be cleaned up after every test */
|
||||||
|
let pokemon: EnemyPokemon | null = null;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
// Game manager can be reused between tests as we are not really modifying the global state
|
||||||
|
// So there is no need to put this in a beforeEach with cleanup in afterEach.
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
pokemon?.destroy();
|
||||||
|
});
|
||||||
|
// Sanitize the interceptor after running the suite to ensure other tests are not affected
|
||||||
|
afterAll(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
//#endregion boilerplate
|
||||||
|
|
||||||
|
function createCharmander(_ = pokemon): asserts _ is EnemyPokemon {
|
||||||
|
pokemon?.destroy();
|
||||||
|
pokemon = createTestablePokemon(SpeciesId.CHARMANDER, { level: 10 });
|
||||||
|
expect(pokemon).toBeInstanceOf(EnemyPokemon);
|
||||||
|
}
|
||||||
|
describe("getAndWeightLevelMoves", () => {
|
||||||
|
const { getAndWeightLevelMoves } = __INTERNAL_TEST_EXPORTS;
|
||||||
|
|
||||||
|
it("returns an empty map if getLevelMoves throws", async () => {
|
||||||
|
createCharmander(pokemon);
|
||||||
|
vi.spyOn(pokemon, "getLevelMoves").mockImplementation(() => {
|
||||||
|
throw new Error("fail");
|
||||||
|
});
|
||||||
|
// Suppress the warning from the test output
|
||||||
|
const warnMock = vi.spyOn(console, "warn").mockImplementationOnce(() => {});
|
||||||
|
|
||||||
|
const result = getAndWeightLevelMoves(pokemon);
|
||||||
|
expect(warnMock).toHaveBeenCalled();
|
||||||
|
expect(result.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips unimplemented moves", () => {
|
||||||
|
createCharmander(pokemon);
|
||||||
|
vi.spyOn(pokemon, "getLevelMoves").mockReturnValue([
|
||||||
|
[1, MoveId.TACKLE],
|
||||||
|
[5, MoveId.GROWL],
|
||||||
|
]);
|
||||||
|
vi.spyOn(allMoves[MoveId.TACKLE], "name", "get").mockReturnValue("Tackle (N)");
|
||||||
|
const result = getAndWeightLevelMoves(pokemon);
|
||||||
|
expect(result.has(MoveId.TACKLE)).toBe(false);
|
||||||
|
expect(result.has(MoveId.GROWL)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips moves already in the pool", () => {
|
||||||
|
createCharmander(pokemon);
|
||||||
|
vi.spyOn(pokemon, "getLevelMoves").mockReturnValue([
|
||||||
|
[1, MoveId.TACKLE],
|
||||||
|
[5, MoveId.TACKLE],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = getAndWeightLevelMoves(pokemon);
|
||||||
|
expect(result.get(MoveId.TACKLE)).toBe(21);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("weights moves based on level", () => {
|
||||||
|
createCharmander(pokemon);
|
||||||
|
vi.spyOn(pokemon, "getLevelMoves").mockReturnValue([
|
||||||
|
[1, MoveId.TACKLE],
|
||||||
|
[5, MoveId.GROWL],
|
||||||
|
[9, MoveId.EMBER],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = getAndWeightLevelMoves(pokemon);
|
||||||
|
expect(result.get(MoveId.TACKLE)).toBe(21);
|
||||||
|
expect(result.get(MoveId.GROWL)).toBe(25);
|
||||||
|
expect(result.get(MoveId.EMBER)).toBe(29);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Regression Tests - ai-moveset-gen.ts", () => {
|
||||||
|
//#region boilerplate
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
/**A pokemon object that will be cleaned up after every test */
|
||||||
|
let pokemon: EnemyPokemon | null = null;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
// Game manager can be reused between tests as we are not really modifying the global state
|
||||||
|
// So there is no need to put this in a beforeEach with cleanup in afterEach.
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
pokemon?.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
//#endregion boilerplate
|
||||||
|
|
||||||
|
describe("getTmPoolForSpecies", () => {
|
||||||
|
const { getTmPoolForSpecies } = __INTERNAL_TEST_EXPORTS;
|
||||||
|
|
||||||
|
it("should not crash when generating a moveset for Pokemon without TM moves", () => {
|
||||||
|
pokemon = createTestablePokemon(SpeciesId.DITTO, { level: 50 });
|
||||||
|
expect(() =>
|
||||||
|
getTmPoolForSpecies(SpeciesId.DITTO, ULTRA_TIER_TM_LEVEL_REQUIREMENT, "", new Map(), new Map(), new Map(), [
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
]),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -49,7 +49,7 @@ describe("BattlerTag - SubstituteTag", () => {
|
|||||||
|
|
||||||
vi.spyOn(messages, "getPokemonNameWithAffix").mockReturnValue("");
|
vi.spyOn(messages, "getPokemonNameWithAffix").mockReturnValue("");
|
||||||
vi.spyOn(mockPokemon.scene as BattleScene, "getPokemonById").mockImplementation(pokemonId =>
|
vi.spyOn(mockPokemon.scene as BattleScene, "getPokemonById").mockImplementation(pokemonId =>
|
||||||
mockPokemon.id === pokemonId ? mockPokemon : null,
|
mockPokemon.id === pokemonId ? mockPokemon : undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -202,7 +202,7 @@ describe("Egg Generation Tests", () => {
|
|||||||
const scene = game.scene;
|
const scene = game.scene;
|
||||||
|
|
||||||
const eggMoveIndex = new Egg({ scene }).eggMoveIndex;
|
const eggMoveIndex = new Egg({ scene }).eggMoveIndex;
|
||||||
const result = !Utils.isNullOrUndefined(eggMoveIndex) && eggMoveIndex >= 0 && eggMoveIndex <= 3;
|
const result = eggMoveIndex != null && eggMoveIndex >= 0 && eggMoveIndex <= 3;
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
@ -10,6 +10,7 @@ import { PokemonType } from "#enums/pokemon-type";
|
|||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
import { TrainerType } from "#enums/trainer-type";
|
import { TrainerType } from "#enums/trainer-type";
|
||||||
|
import { TrainerVariant } from "#enums/trainer-variant";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
@ -193,7 +194,7 @@ describe("Moves - Whirlwind", () => {
|
|||||||
.battleType(BattleType.TRAINER)
|
.battleType(BattleType.TRAINER)
|
||||||
.randomTrainer({
|
.randomTrainer({
|
||||||
trainerType: TrainerType.BREEDER,
|
trainerType: TrainerType.BREEDER,
|
||||||
alwaysDouble: true,
|
trainerVariant: TrainerVariant.DOUBLE,
|
||||||
})
|
})
|
||||||
.enemyMoveset([MoveId.SPLASH, MoveId.LUNAR_DANCE])
|
.enemyMoveset([MoveId.SPLASH, MoveId.LUNAR_DANCE])
|
||||||
.moveset([MoveId.WHIRLWIND, MoveId.SPLASH]);
|
.moveset([MoveId.WHIRLWIND, MoveId.SPLASH]);
|
||||||
|
@ -17,7 +17,6 @@ import type { MessageUiHandler } from "#ui/message-ui-handler";
|
|||||||
import type { MysteryEncounterUiHandler } from "#ui/mystery-encounter-ui-handler";
|
import type { MysteryEncounterUiHandler } from "#ui/mystery-encounter-ui-handler";
|
||||||
import type { OptionSelectUiHandler } from "#ui/option-select-ui-handler";
|
import type { OptionSelectUiHandler } from "#ui/option-select-ui-handler";
|
||||||
import type { PartyUiHandler } from "#ui/party-ui-handler";
|
import type { PartyUiHandler } from "#ui/party-ui-handler";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
import { expect, vi } from "vitest";
|
import { expect, vi } from "vitest";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -147,7 +146,7 @@ export async function runSelectMysteryEncounterOption(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isNullOrUndefined(secondaryOptionSelect?.pokemonNo)) {
|
if (secondaryOptionSelect?.pokemonNo != null) {
|
||||||
await handleSecondaryOptionSelect(game, secondaryOptionSelect.pokemonNo, secondaryOptionSelect.optionNo);
|
await handleSecondaryOptionSelect(game, secondaryOptionSelect.pokemonNo, secondaryOptionSelect.optionNo);
|
||||||
} else {
|
} else {
|
||||||
uiHandler.processInput(Button.ACTION);
|
uiHandler.processInput(Button.ACTION);
|
||||||
@ -174,7 +173,7 @@ async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number,
|
|||||||
partyUiHandler.processInput(Button.ACTION);
|
partyUiHandler.processInput(Button.ACTION);
|
||||||
|
|
||||||
// If there is a second choice to make after selecting a Pokemon
|
// If there is a second choice to make after selecting a Pokemon
|
||||||
if (!isNullOrUndefined(optionNo)) {
|
if (optionNo != null) {
|
||||||
// Wait for Summary menu to close and second options to spawn
|
// Wait for Summary menu to close and second options to spawn
|
||||||
const secondOptionUiHandler = game.scene.ui.handlers[UiMode.OPTION_SELECT] as OptionSelectUiHandler;
|
const secondOptionUiHandler = game.scene.ui.handlers[UiMode.OPTION_SELECT] as OptionSelectUiHandler;
|
||||||
vi.spyOn(secondOptionUiHandler, "show");
|
vi.spyOn(secondOptionUiHandler, "show");
|
||||||
|
@ -52,7 +52,6 @@ import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler";
|
|||||||
import type { PartyUiHandler } from "#ui/party-ui-handler";
|
import type { PartyUiHandler } from "#ui/party-ui-handler";
|
||||||
import type { StarterSelectUiHandler } from "#ui/starter-select-ui-handler";
|
import type { StarterSelectUiHandler } from "#ui/starter-select-ui-handler";
|
||||||
import type { TargetSelectUiHandler } from "#ui/target-select-ui-handler";
|
import type { TargetSelectUiHandler } from "#ui/target-select-ui-handler";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { AES, enc } from "crypto-js";
|
import { AES, enc } from "crypto-js";
|
||||||
import { expect, vi } from "vitest";
|
import { expect, vi } from "vitest";
|
||||||
@ -240,7 +239,7 @@ export class GameManager {
|
|||||||
* @returns A Promise that resolves when the EncounterPhase ends.
|
* @returns A Promise that resolves when the EncounterPhase ends.
|
||||||
*/
|
*/
|
||||||
async runToMysteryEncounter(encounterType?: MysteryEncounterType, species?: SpeciesId[]) {
|
async runToMysteryEncounter(encounterType?: MysteryEncounterType, species?: SpeciesId[]) {
|
||||||
if (!isNullOrUndefined(encounterType)) {
|
if (encounterType != null) {
|
||||||
this.override.disableTrainerWaves();
|
this.override.disableTrainerWaves();
|
||||||
this.override.mysteryEncounter(encounterType);
|
this.override.mysteryEncounter(encounterType);
|
||||||
}
|
}
|
||||||
@ -272,7 +271,7 @@ export class GameManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await this.phaseInterceptor.to("EncounterPhase");
|
await this.phaseInterceptor.to("EncounterPhase");
|
||||||
if (!isNullOrUndefined(encounterType)) {
|
if (encounterType != null) {
|
||||||
expect(this.scene.currentBattle?.mysteryEncounter?.encounterType).toBe(encounterType);
|
expect(this.scene.currentBattle?.mysteryEncounter?.encounterType).toBe(encounterType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Code to add markers to the beginning and end of tests.
|
||||||
|
* Intended for use with {@linkcode CustomDefaultReporter}, and placed inside test hooks
|
||||||
|
* (rather than as part of the reporter) to ensure Vitest waits for the log messages to be printed.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||||
import type CustomDefaultReporter from "#test/test-utils/reporters/custom-default-reporter";
|
import type CustomDefaultReporter from "#test/test-utils/reporters/custom-default-reporter";
|
||||||
import { basename, join, relative } from "path";
|
import { basename, join, relative } from "path";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import type { RunnerTask, RunnerTaskResult, RunnerTestCase } from "vitest";
|
import type { RunnerTask, RunnerTaskResult, RunnerTestCase } from "vitest";
|
||||||
|
|
||||||
/**
|
|
||||||
* @module
|
|
||||||
* Code to add markers to the beginning and end of tests.
|
|
||||||
* Intended for use with {@linkcode CustomDefaultReporter}, and placed inside test hooks
|
|
||||||
* (rather than as part of the reporter) to ensure Vitest waits for the log messages to be printed.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** A long string of "="s to partition off each test from one another. */
|
/** A long string of "="s to partition off each test from one another. */
|
||||||
const TEST_END_BARRIER = chalk.bold.hex("#ff7c7cff")("==================");
|
const TEST_END_BARRIER = chalk.bold.hex("#ff7c7cff")("==================");
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user