Merge remote-tracking branch 'upstream/beta' into battle-move-flyout
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": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
@ -98,7 +98,9 @@
|
||||
"useTrimStartEnd": "error",
|
||||
"useReadonlyClassProperties": {
|
||||
"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": {
|
||||
"level": "error",
|
||||
@ -209,11 +211,15 @@
|
||||
"nursery": {
|
||||
"noUselessUndefined": "error",
|
||||
"useMaxParams": {
|
||||
"level": "warn", // TODO: Change to "error"... eventually...
|
||||
"options": { "max": 4 } // A lot of stuff has a few params, but
|
||||
"level": "info", // TODO: Change to "error"... eventually...
|
||||
"options": { "max": 7 }
|
||||
},
|
||||
"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),
|
||||
// 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",
|
||||
"**/boilerplates/*.ts"
|
||||
],
|
||||
"includes": ["**/src/overrides.ts", "**/src/enums/**/*", "**/*.d.ts", "scripts/**/*.boilerplate.ts"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"correctness": {
|
||||
|
11
package.json
@ -10,6 +10,7 @@
|
||||
"start:podman": "vite --mode development --host 0.0.0.0 --port $PORT",
|
||||
"build": "vite build",
|
||||
"build:beta": "vite build --mode beta",
|
||||
"build:dev": "vite build --mode development",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run --no-isolate",
|
||||
"test:cov": "vitest run --coverage --no-isolate",
|
||||
@ -32,7 +33,7 @@
|
||||
"update-locales:remote": "git submodule update --progress --init --recursive --force --remote"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.3",
|
||||
"@biomejs/biome": "2.2.4",
|
||||
"@ls-lint/ls-lint": "2.3.1",
|
||||
"@types/crypto-js": "^4.2.0",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
@ -47,12 +48,12 @@
|
||||
"lefthook": "^1.12.2",
|
||||
"msw": "^2.10.4",
|
||||
"phaser3spectorjs": "^0.0.8",
|
||||
"typedoc": "0.28.7",
|
||||
"typedoc": "^0.28.13",
|
||||
"typedoc-github-theme": "^0.3.1",
|
||||
"typedoc-plugin-coverage": "^4.0.1",
|
||||
"typedoc-plugin-mdn-links": "^5.0.9",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.0.6",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.0.7",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.4",
|
||||
"vitest-canvas-mock": "^0.3.3"
|
||||
@ -73,5 +74,5 @@
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.14.0"
|
||||
"packageManager": "pnpm@10.17.0"
|
||||
}
|
||||
|
637
pnpm-lock.yaml
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 202 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 124 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 235 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 186 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 180 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 181 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 242 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 198 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 196 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 183 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 169 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 202 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 249 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 190 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 244 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 195 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 235 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 193 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 180 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 197 B |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 307 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 202 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 249 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 190 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 244 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 195 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 235 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 193 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 180 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 197 B |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 307 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 202 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 124 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 235 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 187 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 180 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 181 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 242 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 198 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 196 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 183 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 169 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 124 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 235 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 187 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 180 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 181 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 242 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 198 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 196 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 183 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 169 B |
@ -47,6 +47,6 @@ describe("{{description}}", () => {
|
||||
await game.toEndOfTurn();
|
||||
|
||||
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) {
|
||||
switch (choiceType) {
|
||||
// case "Reward":
|
||||
// return path.join(__dirname, "boilerplates/reward.ts");
|
||||
// return path.join(__dirname, "boilerplates/reward.boilerplate.ts");
|
||||
default:
|
||||
return path.join(__dirname, "boilerplates/default.ts");
|
||||
return path.join(__dirname, "boilerplates/default.boilerplate.ts");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,8 @@ export type AbilityBattlerTagType =
|
||||
| BattlerTagType.QUARK_DRIVE
|
||||
| BattlerTagType.UNBURDEN
|
||||
| BattlerTagType.SLOW_START
|
||||
| BattlerTagType.TRUANT;
|
||||
| BattlerTagType.TRUANT
|
||||
| BattlerTagType.SUPREME_OVERLORD;
|
||||
|
||||
/** Subset of {@linkcode BattlerTagType}s that provide type boosts */
|
||||
export type TypeBoostTagType = BattlerTagType.FIRE_BOOST | BattlerTagType.CHARGED;
|
||||
|
44
src/@types/damage-params.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import type { MoveCategory } from "#enums/move-category";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import type { Move } from "#types/move-types";
|
||||
|
||||
/**
|
||||
* Collection of types for methods like {@linkcode Pokemon#getBaseDamage} and {@linkcode Pokemon#getAttackDamage}.
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** Base type for damage parameter methods, used for DRY */
|
||||
export interface damageParams {
|
||||
/** The attacking {@linkcode Pokemon} */
|
||||
source: Pokemon;
|
||||
/** The move used in the attack */
|
||||
move: Move;
|
||||
/** The move's {@linkcode MoveCategory} after variable-category effects are applied */
|
||||
moveCategory: MoveCategory;
|
||||
/** If `true`, ignores this Pokemon's defensive ability effects */
|
||||
ignoreAbility?: boolean;
|
||||
/** If `true`, ignores the attacking Pokemon's ability effects */
|
||||
ignoreSourceAbility?: boolean;
|
||||
/** If `true`, ignores the ally Pokemon's ability effects */
|
||||
ignoreAllyAbility?: boolean;
|
||||
/** If `true`, ignores the ability effects of the attacking pokemon's ally */
|
||||
ignoreSourceAllyAbility?: boolean;
|
||||
/** If `true`, calculates damage for a critical hit */
|
||||
isCritical?: boolean;
|
||||
/** If `true`, suppresses changes to game state during the calculation */
|
||||
simulated?: boolean;
|
||||
/** If defined, used in place of calculated effectiveness values */
|
||||
effectiveness?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for the parameters of {@linkcode Pokemon#getBaseDamage | getBaseDamage}
|
||||
* @interface
|
||||
*/
|
||||
export type getBaseDamageParams = Omit<damageParams, "effectiveness">;
|
||||
|
||||
/**
|
||||
* Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage}
|
||||
* @interface
|
||||
*/
|
||||
export type getAttackDamageParams = Omit<damageParams, "moveCategory">;
|
@ -1,26 +1,27 @@
|
||||
import type { Pokemon } from "#app/field/pokemon";
|
||||
import type { Phase } from "#app/phase";
|
||||
import type { PhaseConstructorMap } from "#app/phase-manager";
|
||||
import type { ObjectValues } from "#types/type-helpers";
|
||||
|
||||
// Intentionally export the types of everything in phase-manager, as this file is meant to be
|
||||
// Intentionally [re-]export the types of everything in phase-manager, as this file is meant to be
|
||||
// the centralized place for type definitions for the phase system.
|
||||
export type * from "#app/phase-manager";
|
||||
|
||||
// This file includes helpful types for the phase system.
|
||||
// It intentionally imports the phase constructor map from the phase manager (and re-exports it)
|
||||
|
||||
/**
|
||||
* Map of phase names to constructors for said phase
|
||||
*/
|
||||
/** Map of phase names to constructors for said phase */
|
||||
export type PhaseMap = {
|
||||
[K in keyof PhaseConstructorMap]: InstanceType<PhaseConstructorMap[K]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Union type of all phase constructors.
|
||||
*/
|
||||
/** Union type of all phase constructors. */
|
||||
export type PhaseClass = ObjectValues<PhaseConstructorMap>;
|
||||
|
||||
/**
|
||||
* Union type of all phase names as strings.
|
||||
*/
|
||||
/** Union type of all phase names as strings. */
|
||||
export type PhaseString = keyof PhaseMap;
|
||||
|
||||
/** Type for predicate functions operating on a specific type of {@linkcode Phase}. */
|
||||
export type PhaseConditionFunc<T extends PhaseString> = (phase: PhaseMap[T]) => boolean;
|
||||
|
||||
/** Interface type representing the assumption that all phases with pokemon associated are dynamic */
|
||||
export interface DynamicPhase extends Phase {
|
||||
getPokemon(): Pokemon;
|
||||
}
|
||||
|
@ -4,8 +4,10 @@ import type { BattleType } from "#enums/battle-type";
|
||||
import type { GameModes } from "#enums/game-modes";
|
||||
import type { MoveId } from "#enums/move-id";
|
||||
import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import type { Nature } from "#enums/nature";
|
||||
import type { PlayerGender } from "#enums/player-gender";
|
||||
import type { PokemonType } from "#enums/pokemon-type";
|
||||
import type { SpeciesId } from "#enums/species-id";
|
||||
import type { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data";
|
||||
import type { Variant } from "#sprites/variant";
|
||||
import type { ArenaData } from "#system/arena-data";
|
||||
@ -108,6 +110,22 @@ export interface DexAttrProps {
|
||||
formIndex: number;
|
||||
}
|
||||
|
||||
export interface Starter {
|
||||
speciesId: SpeciesId;
|
||||
shiny: boolean;
|
||||
variant: Variant;
|
||||
formIndex: number;
|
||||
female?: boolean;
|
||||
abilityIndex: number;
|
||||
passive: boolean;
|
||||
nature: Nature;
|
||||
moveset?: StarterMoveset;
|
||||
pokerus: boolean;
|
||||
nickname?: string;
|
||||
teraType?: PokemonType;
|
||||
ivs: number[];
|
||||
}
|
||||
|
||||
export type RunHistoryData = Record<number, RunEntry>;
|
||||
|
||||
export interface RunEntry {
|
||||
|
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,
|
||||
});
|
||||
}
|
@ -105,7 +105,6 @@ import {
|
||||
import { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
|
||||
import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data";
|
||||
import { allMysteryEncounters, mysteryEncountersByBiome } from "#mystery-encounters/mystery-encounters";
|
||||
import type { MovePhase } from "#phases/move-phase";
|
||||
import { expSpriteKeys } from "#sprites/sprite-keys";
|
||||
import { hasExpSprite } from "#sprites/sprite-utils";
|
||||
import type { Variant } from "#sprites/variant";
|
||||
@ -140,7 +139,6 @@ import {
|
||||
formatMoney,
|
||||
getIvsFromId,
|
||||
isBetween,
|
||||
isNullOrUndefined,
|
||||
NumberHolder,
|
||||
randomString,
|
||||
randSeedInt,
|
||||
@ -784,12 +782,14 @@ export class BattleScene extends SceneBase {
|
||||
|
||||
/**
|
||||
* Returns an array of EnemyPokemon of length 1 or 2 depending on if in a double battle or not.
|
||||
* Does not actually check if the pokemon are on the field or not.
|
||||
* @param active - (Default `false`) Whether to consider only {@linkcode Pokemon.isActive | active} on-field pokemon
|
||||
* @returns array of {@linkcode EnemyPokemon}
|
||||
*/
|
||||
public getEnemyField(): EnemyPokemon[] {
|
||||
public getEnemyField(active = false): EnemyPokemon[] {
|
||||
const party = this.getEnemyParty();
|
||||
return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1));
|
||||
return party
|
||||
.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1))
|
||||
.filter(p => !active || p.isActive());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -814,25 +814,7 @@ export class BattleScene extends SceneBase {
|
||||
* @param allyPokemon - The {@linkcode Pokemon} allied with the removed Pokemon; will have moves redirected to it
|
||||
*/
|
||||
redirectPokemonMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
|
||||
// failsafe: if not a double battle just return
|
||||
if (this.currentBattle.double === false) {
|
||||
return;
|
||||
}
|
||||
if (allyPokemon?.isActive(true)) {
|
||||
let targetingMovePhase: MovePhase;
|
||||
do {
|
||||
targetingMovePhase = this.phaseManager.findPhase(
|
||||
mp =>
|
||||
mp.is("MovePhase")
|
||||
&& mp.targets.length === 1
|
||||
&& mp.targets[0] === removedPokemon.getBattlerIndex()
|
||||
&& mp.pokemon.isPlayer() !== allyPokemon.isPlayer(),
|
||||
) as MovePhase;
|
||||
if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) {
|
||||
targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex();
|
||||
}
|
||||
} while (targetingMovePhase);
|
||||
}
|
||||
this.phaseManager.redirectMoves(removedPokemon, allyPokemon);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -855,20 +837,21 @@ export class BattleScene extends SceneBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@linkcode Pokemon} associated with a given ID.
|
||||
* @param pokemonId - The ID whose Pokemon will be retrieved.
|
||||
* @returns The {@linkcode Pokemon} associated with the given id.
|
||||
* Returns `null` if the ID is `undefined` or not present in either party.
|
||||
* @todo Change the `null` to `undefined` and update callers' signatures -
|
||||
* this is weird and causes a lot of random jank
|
||||
* Return the {@linkcode Pokemon} associated with the given ID.
|
||||
* @param pokemonId - The PID whose Pokemon will be retrieved
|
||||
* @returns The `Pokemon` associated with the given ID,
|
||||
* or `undefined` if none is found in either team's party.
|
||||
* @see {@linkcode Pokemon.id}
|
||||
* @todo `pokemonId` should not allow `undefined`
|
||||
*/
|
||||
getPokemonById(pokemonId: number | undefined): Pokemon | null {
|
||||
if (isNullOrUndefined(pokemonId)) {
|
||||
return null;
|
||||
public getPokemonById(pokemonId: number | undefined): Pokemon | undefined {
|
||||
if (pokemonId == null) {
|
||||
// biome-ignore lint/nursery/noUselessUndefined: More explicit
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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(
|
||||
@ -1315,7 +1298,7 @@ export class BattleScene extends SceneBase {
|
||||
if (
|
||||
!this.gameMode.hasTrainers
|
||||
|| Overrides.BATTLE_TYPE_OVERRIDE === BattleType.WILD
|
||||
|| (Overrides.DISABLE_STANDARD_TRAINERS_OVERRIDE && isNullOrUndefined(trainerData))
|
||||
|| (Overrides.DISABLE_STANDARD_TRAINERS_OVERRIDE && trainerData == null)
|
||||
) {
|
||||
newBattleType = BattleType.WILD;
|
||||
} else {
|
||||
@ -1328,13 +1311,12 @@ export class BattleScene extends SceneBase {
|
||||
if (newBattleType === BattleType.TRAINER) {
|
||||
const trainerType =
|
||||
Overrides.RANDOM_TRAINER_OVERRIDE?.trainerType ?? this.arena.randomTrainerType(newWaveIndex);
|
||||
const hasDouble = trainerConfigs[trainerType].hasDouble;
|
||||
let doubleTrainer = false;
|
||||
if (trainerConfigs[trainerType].doubleOnly) {
|
||||
doubleTrainer = true;
|
||||
} else if (trainerConfigs[trainerType].hasDouble) {
|
||||
doubleTrainer =
|
||||
Overrides.RANDOM_TRAINER_OVERRIDE?.alwaysDouble
|
||||
|| !randSeedInt(this.getDoubleBattleChance(newWaveIndex, playerField));
|
||||
} else if (hasDouble) {
|
||||
doubleTrainer = !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
|
||||
if (
|
||||
trainerConfigs[trainerType].trainerTypeDouble
|
||||
@ -1343,11 +1325,19 @@ export class BattleScene extends SceneBase {
|
||||
doubleTrainer = false;
|
||||
}
|
||||
}
|
||||
const variant = doubleTrainer
|
||||
? TrainerVariant.DOUBLE
|
||||
: randSeedInt(2)
|
||||
? TrainerVariant.FEMALE
|
||||
: TrainerVariant.DEFAULT;
|
||||
|
||||
// Forcing a double battle on wave 1 causes a bug where only one enemy is sent out,
|
||||
// making it impossible to complete the fight without a reload
|
||||
const overrideVariant =
|
||||
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);
|
||||
this.field.add(newTrainer);
|
||||
}
|
||||
@ -1379,7 +1369,7 @@ export class BattleScene extends SceneBase {
|
||||
newDouble = false;
|
||||
}
|
||||
|
||||
if (!isNullOrUndefined(Overrides.BATTLE_STYLE_OVERRIDE)) {
|
||||
if (Overrides.BATTLE_STYLE_OVERRIDE != null) {
|
||||
let doubleOverrideForWave: "single" | "double" | null = null;
|
||||
|
||||
switch (Overrides.BATTLE_STYLE_OVERRIDE) {
|
||||
@ -1422,7 +1412,7 @@ export class BattleScene extends SceneBase {
|
||||
}
|
||||
|
||||
if (lastBattle?.double && !newDouble) {
|
||||
this.phaseManager.tryRemovePhase((p: Phase) => p.is("SwitchPhase"));
|
||||
this.phaseManager.tryRemovePhase("SwitchPhase");
|
||||
for (const p of this.getPlayerField()) {
|
||||
p.lapseTag(BattlerTagType.COMMANDED);
|
||||
}
|
||||
@ -1568,7 +1558,7 @@ export class BattleScene extends SceneBase {
|
||||
// Give trainers with specialty types an appropriately-typed form for Wormadam, Rotom, Arceus, Oricorio, Silvally, or Paldean Tauros.
|
||||
!isEggPhase
|
||||
&& this.currentBattle?.battleType === BattleType.TRAINER
|
||||
&& !isNullOrUndefined(this.currentBattle.trainer)
|
||||
&& this.currentBattle.trainer != null
|
||||
&& this.currentBattle.trainer.config.hasSpecialtyType()
|
||||
) {
|
||||
if (species.speciesId === SpeciesId.WORMADAM) {
|
||||
@ -2688,7 +2678,7 @@ export class BattleScene extends SceneBase {
|
||||
}
|
||||
} else if (modifier instanceof FusePokemonModifier) {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -3003,7 +2993,7 @@ export class BattleScene extends SceneBase {
|
||||
}
|
||||
if (
|
||||
modifier instanceof PokemonHeldItemModifier
|
||||
&& !isNullOrUndefined(modifier.getSpecies())
|
||||
&& modifier.getSpecies() != null
|
||||
&& !this.getPokemonById(modifier.pokemonId)?.hasSpecies(modifier.getSpecies()!)
|
||||
) {
|
||||
modifiers.splice(m--, 1);
|
||||
@ -3569,7 +3559,7 @@ export class BattleScene extends SceneBase {
|
||||
// Loading override or session encounter
|
||||
let encounter: MysteryEncounter | null;
|
||||
if (
|
||||
!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_OVERRIDE)
|
||||
Overrides.MYSTERY_ENCOUNTER_OVERRIDE != null
|
||||
&& allMysteryEncounters.hasOwnProperty(Overrides.MYSTERY_ENCOUNTER_OVERRIDE)
|
||||
) {
|
||||
encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE];
|
||||
@ -3580,7 +3570,7 @@ export class BattleScene extends SceneBase {
|
||||
encounter = allMysteryEncounters[encounterType ?? -1];
|
||||
return encounter;
|
||||
} else {
|
||||
encounter = !isNullOrUndefined(encounterType) ? allMysteryEncounters[encounterType] : null;
|
||||
encounter = encounterType != null ? allMysteryEncounters[encounterType] : null;
|
||||
}
|
||||
|
||||
// Check for queued encounters first
|
||||
@ -3639,7 +3629,7 @@ export class BattleScene extends SceneBase {
|
||||
? MysteryEncounterTier.ULTRA
|
||||
: MysteryEncounterTier.ROGUE;
|
||||
|
||||
if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE)) {
|
||||
if (Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE != null) {
|
||||
tier = Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
/**
|
||||
* @module
|
||||
*
|
||||
* A big file storing colors used in logging.
|
||||
* Minified by Terser during production builds, so has no overhead.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// Colors used in prod
|
||||
|
@ -33,6 +33,7 @@ import { CommonAnim } from "#enums/move-anims-common";
|
||||
import { MoveCategory } from "#enums/move-category";
|
||||
import { MoveFlags } from "#enums/move-flags";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { MoveTarget } from "#enums/move-target";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
@ -66,7 +67,6 @@ import type { Constructor } from "#utils/common";
|
||||
import {
|
||||
BooleanHolder,
|
||||
coerceArray,
|
||||
isNullOrUndefined,
|
||||
NumberHolder,
|
||||
randSeedFloat,
|
||||
randSeedInt,
|
||||
@ -1039,7 +1039,7 @@ export class PostDefendStatStageChangeAbAttr extends PostDefendAbAttr {
|
||||
|
||||
if (this.allOthers) {
|
||||
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) {
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"StatStageChangePhase",
|
||||
@ -1472,7 +1472,7 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr {
|
||||
|
||||
override canApply({ move, opponent: attacker, pokemon }: PostMoveInteractionAbAttrParams): boolean {
|
||||
return (
|
||||
isNullOrUndefined(attacker.getTag(BattlerTagType.DISABLED))
|
||||
attacker.getTag(BattlerTagType.DISABLED) == null
|
||||
&& move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon })
|
||||
&& (this.chance === -1 || pokemon.randBattleSeedInt(100) < this.chance)
|
||||
);
|
||||
@ -2555,7 +2555,7 @@ export class PostIntimidateStatStageChangeAbAttr extends AbAttr {
|
||||
|
||||
override apply({ pokemon, simulated, cancelled }: AbAttrParamsWithCancel): void {
|
||||
if (!simulated) {
|
||||
globalScene.phaseManager.pushNew(
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"StatStageChangePhase",
|
||||
pokemon.getBattlerIndex(),
|
||||
false,
|
||||
@ -2809,7 +2809,7 @@ export class PostSummonAllyHealAbAttr extends PostSummonAbAttr {
|
||||
|
||||
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
|
||||
const target = pokemon.getAlly();
|
||||
if (!simulated && !isNullOrUndefined(target)) {
|
||||
if (!simulated && target != null) {
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"PokemonHealPhase",
|
||||
target.getBattlerIndex(),
|
||||
@ -2840,7 +2840,7 @@ export class PostSummonClearAllyStatStagesAbAttr extends PostSummonAbAttr {
|
||||
|
||||
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
|
||||
const target = pokemon.getAlly();
|
||||
if (!simulated && !isNullOrUndefined(target)) {
|
||||
if (!simulated && target != null) {
|
||||
for (const s of BATTLE_STATS) {
|
||||
target.setStatStage(s, 0);
|
||||
}
|
||||
@ -2959,13 +2959,13 @@ export class PostSummonHealStatusAbAttr extends PostSummonRemoveEffectAbAttr {
|
||||
|
||||
public override canApply({ pokemon }: AbAttrBaseParams): boolean {
|
||||
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 {
|
||||
// TODO: should probably check against simulated...
|
||||
const status = pokemon.status?.effect;
|
||||
if (!isNullOrUndefined(status)) {
|
||||
if (status != null) {
|
||||
this.statusHealed = status;
|
||||
pokemon.resetStatus(false);
|
||||
pokemon.updateInfo();
|
||||
@ -3101,7 +3101,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -3109,7 +3109,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
|
||||
return;
|
||||
}
|
||||
const ally = pokemon.getAlly();
|
||||
if (!isNullOrUndefined(ally)) {
|
||||
if (ally != null) {
|
||||
for (const s of BATTLE_STATS) {
|
||||
pokemon.setStatStage(s, ally.getStatStage(s));
|
||||
}
|
||||
@ -3239,7 +3239,8 @@ export class CommanderAbAttr extends AbAttr {
|
||||
const ally = pokemon.getAlly();
|
||||
return (
|
||||
globalScene.currentBattle?.double
|
||||
&& !isNullOrUndefined(ally)
|
||||
&& ally != null
|
||||
&& ally.isActive(true)
|
||||
&& ally.species.speciesId === SpeciesId.DONDOZO
|
||||
&& !(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED))
|
||||
);
|
||||
@ -3254,7 +3255,7 @@ export class CommanderAbAttr extends AbAttr {
|
||||
// Apply boosts from this effect to the ally Dondozo
|
||||
pokemon.getAlly()?.addTag(BattlerTagType.COMMANDED, 0, MoveId.NONE, pokemon.id);
|
||||
// Cancel the source Pokemon's next move (if a move is queued)
|
||||
globalScene.phaseManager.tryRemovePhase(phase => phase.is("MovePhase") && phase.pokemon === pokemon);
|
||||
globalScene.phaseManager.tryRemovePhase("MovePhase", phase => phase.pokemon === pokemon);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3283,7 +3284,7 @@ export class PreSwitchOutResetStatusAbAttr extends PreSwitchOutAbAttr {
|
||||
}
|
||||
|
||||
override canApply({ pokemon }: AbAttrBaseParams): boolean {
|
||||
return !isNullOrUndefined(pokemon.status);
|
||||
return pokemon.status != null;
|
||||
}
|
||||
|
||||
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
|
||||
@ -3563,7 +3564,7 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
|
||||
}
|
||||
|
||||
override canApply({ stat, cancelled }: PreStatStageChangeAbAttrParams): boolean {
|
||||
return !cancelled.value && (isNullOrUndefined(this.protectedStat) || stat === this.protectedStat);
|
||||
return !cancelled.value && (this.protectedStat == null || stat === this.protectedStat);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3799,11 +3800,7 @@ export class ConditionalUserFieldProtectStatAbAttr extends PreStatStageChangeAbA
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
!cancelled.value
|
||||
&& (isNullOrUndefined(this.protectedStat) || stat === this.protectedStat)
|
||||
&& this.condition(target)
|
||||
);
|
||||
return !cancelled.value && (this.protectedStat == null || stat === this.protectedStat) && this.condition(target);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -4560,7 +4557,7 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr {
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -4893,7 +4890,7 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr {
|
||||
*/
|
||||
export class FetchBallAbAttr extends PostTurnAbAttr {
|
||||
override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean {
|
||||
return !simulated && !isNullOrUndefined(globalScene.currentBattle.lastUsedPokeball) && !!pokemon.isPlayer;
|
||||
return !simulated && globalScene.currentBattle.lastUsedPokeball != null && !!pokemon.isPlayer;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -5006,7 +5003,14 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
|
||||
// If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance
|
||||
if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) {
|
||||
const target = this.getTarget(pokemon, source, targets);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", pokemon, target, move, MoveUseMode.INDIRECT);
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"MovePhase",
|
||||
pokemon,
|
||||
target,
|
||||
move,
|
||||
MoveUseMode.INDIRECT,
|
||||
MovePhaseTimingModifier.FIRST,
|
||||
);
|
||||
} else if (move.getMove().is("SelfStatusMove")) {
|
||||
// If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
@ -5015,6 +5019,7 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
|
||||
[pokemon.getBattlerIndex()],
|
||||
move,
|
||||
MoveUseMode.INDIRECT,
|
||||
MovePhaseTimingModifier.FIRST,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -6030,11 +6035,6 @@ export class IllusionPostBattleAbAttr extends PostBattleAbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
export interface BypassSpeedChanceAbAttrParams extends AbAttrBaseParams {
|
||||
/** Holds whether the speed check is bypassed after ability application */
|
||||
bypass: BooleanHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a Pokémon with this Ability selects a damaging move, it has a 30% chance of going first in its priority bracket. If the Ability activates, this is announced at the start of the turn (after move selection).
|
||||
* @sealed
|
||||
@ -6050,26 +6050,28 @@ export class BypassSpeedChanceAbAttr extends AbAttr {
|
||||
this.chance = chance;
|
||||
}
|
||||
|
||||
override canApply({ bypass, simulated, pokemon }: BypassSpeedChanceAbAttrParams): boolean {
|
||||
override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean {
|
||||
// TODO: Consider whether we can move the simulated check to the `apply` method
|
||||
// May be difficult as we likely do not want to modify the randBattleSeed
|
||||
const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()];
|
||||
const isCommandFight = turnCommand?.command === Command.FIGHT;
|
||||
const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null;
|
||||
const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL;
|
||||
return (
|
||||
!simulated && !bypass.value && pokemon.randBattleSeedInt(100) < this.chance && isCommandFight && isDamageMove
|
||||
!simulated
|
||||
&& pokemon.randBattleSeedInt(100) < this.chance
|
||||
&& isDamageMove
|
||||
&& pokemon.canAddTag(BattlerTagType.BYPASS_SPEED)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* bypass move order in their priority bracket when pokemon choose damaging move
|
||||
*/
|
||||
override apply({ bypass }: BypassSpeedChanceAbAttrParams): void {
|
||||
bypass.value = true;
|
||||
override apply({ pokemon }: AbAttrBaseParams): void {
|
||||
pokemon.addTag(BattlerTagType.BYPASS_SPEED);
|
||||
}
|
||||
|
||||
override getTriggerMessage({ pokemon }: BypassSpeedChanceAbAttrParams, _abilityName: string): string {
|
||||
override getTriggerMessage({ pokemon }: AbAttrBaseParams, _abilityName: string): string {
|
||||
return i18next.t("abilityTriggers:quickDraw", { pokemonName: getPokemonNameWithAffix(pokemon) });
|
||||
}
|
||||
}
|
||||
@ -6077,8 +6079,6 @@ export class BypassSpeedChanceAbAttr extends AbAttr {
|
||||
export interface PreventBypassSpeedChanceAbAttrParams extends AbAttrBaseParams {
|
||||
/** Holds whether the speed check is bypassed after ability application */
|
||||
bypass: BooleanHolder;
|
||||
/** Holds whether the Pokemon can check held items for Quick Claw's effects */
|
||||
canCheckHeldItems: BooleanHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -6105,9 +6105,8 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr {
|
||||
return isCommandFight && this.condition(pokemon, move!);
|
||||
}
|
||||
|
||||
override apply({ bypass, canCheckHeldItems }: PreventBypassSpeedChanceAbAttrParams): void {
|
||||
override apply({ bypass }: PreventBypassSpeedChanceAbAttrParams): void {
|
||||
bypass.value = false;
|
||||
canCheckHeldItems.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6205,8 +6204,7 @@ class ForceSwitchOutHelper {
|
||||
|
||||
if (switchOutTarget.hp > 0) {
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
globalScene.phaseManager.prependNewToPhase(
|
||||
"MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6228,8 +6226,7 @@ class ForceSwitchOutHelper {
|
||||
const summonIndex = globalScene.currentBattle.trainer
|
||||
? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot)
|
||||
: 0;
|
||||
globalScene.phaseManager.prependNewToPhase(
|
||||
"MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchSummonPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6258,7 +6255,7 @@ class ForceSwitchOutHelper {
|
||||
true,
|
||||
500,
|
||||
);
|
||||
if (globalScene.currentBattle.double && !isNullOrUndefined(allyPokemon)) {
|
||||
if (globalScene.currentBattle.double && allyPokemon != null) {
|
||||
globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon);
|
||||
}
|
||||
}
|
||||
@ -6951,7 +6948,7 @@ export function initAbilities() {
|
||||
.attr(TypeImmunityStatStageChangeAbAttr, PokemonType.ELECTRIC, Stat.SPD, 1)
|
||||
.ignorable(),
|
||||
new Ability(AbilityId.RIVALRY, 4)
|
||||
.attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender === target?.gender, 1.25, true)
|
||||
.attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender === target?.gender, 1.25)
|
||||
.attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender !== target?.gender, 0.75),
|
||||
new Ability(AbilityId.STEADFAST, 4)
|
||||
.attr(FlinchStatStageChangeAbAttr, [ Stat.SPD ], 1),
|
||||
@ -7110,7 +7107,7 @@ export function initAbilities() {
|
||||
.attr(PostDefendMoveDisableAbAttr, 30)
|
||||
.bypassFaint(),
|
||||
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)
|
||||
.attr(AlliedFieldDamageReductionAbAttr, 0.75)
|
||||
.ignorable(),
|
||||
@ -7163,7 +7160,7 @@ export function initAbilities() {
|
||||
new Ability(AbilityId.ANALYTIC, 5)
|
||||
.attr(MovePowerBoostAbAttr, (user) =>
|
||||
// Boost power if all other Pokemon have already moved (no other moves are slated to execute)
|
||||
!globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id),
|
||||
!globalScene.phaseManager.hasPhaseOfType("MovePhase", phase => phase.pokemon.id !== user?.id),
|
||||
1.3),
|
||||
new Ability(AbilityId.ILLUSION, 5)
|
||||
// The Pokemon generate an illusion if it's available
|
||||
@ -7744,8 +7741,8 @@ export function initAbilities() {
|
||||
new Ability(AbilityId.SHARPNESS, 9)
|
||||
.attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5),
|
||||
new Ability(AbilityId.SUPREME_OVERLORD, 9)
|
||||
.attr(VariableMovePowerBoostAbAttr, (user, _target, _move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 5))
|
||||
.partial(), // Should only boost once, on summon
|
||||
.conditionalAttr((p) => (p.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints) > 0, PostSummonAddBattlerTagAbAttr, BattlerTagType.SUPREME_OVERLORD, 0, true)
|
||||
.edgeCase(), // Tag is not tied to ability, so suppression/removal etc will not function until a structure to allow this is implemented
|
||||
new Ability(AbilityId.COSTAR, 9, -2)
|
||||
.attr(PostSummonCopyAllyStatsAbAttr),
|
||||
new Ability(AbilityId.TOXIC_DEBRIS, 9)
|
||||
|
@ -74,7 +74,6 @@ function applyAbAttrsInternal<T extends CallableAbAttrString>(
|
||||
for (const passive of [false, true]) {
|
||||
params.passive = passive;
|
||||
applySingleAbAttrs(attrType, params, gainedMidTurn, messages);
|
||||
globalScene.phaseManager.clearPhaseQueueSplice();
|
||||
}
|
||||
// We need to restore passive to its original state in the case that it was undefined on entry
|
||||
// this is necessary in case this method is called with an object that is reused.
|
||||
|
@ -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).
|
||||
* Examples include (but are not limited to)
|
||||
* - Cross-turn effects that persist even if the user/target switches out, such as Happy Hour
|
||||
@ -76,14 +41,54 @@ import i18next from "i18next";
|
||||
* ```
|
||||
* Notes
|
||||
* - 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 type { BattlerIndex } from "#enums/battler-index";
|
||||
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 BaseArenaTag {
|
||||
/**
|
||||
* The tag's remaining duration. Setting to any number `<=0` will make the tag's duration effectively infinite.
|
||||
*/
|
||||
turnCount: number;
|
||||
/**
|
||||
* The tag's max duration.
|
||||
*/
|
||||
maxDuration: number;
|
||||
/**
|
||||
* The {@linkcode MoveId} that created this tag, or `undefined` if not set by a move.
|
||||
*/
|
||||
@ -110,12 +115,14 @@ export abstract class ArenaTag implements BaseArenaTag {
|
||||
/** The type of the arena tag */
|
||||
public abstract readonly tagType: ArenaTagType;
|
||||
public turnCount: number;
|
||||
public maxDuration: number;
|
||||
public sourceMove?: MoveId;
|
||||
public sourceId: number | undefined;
|
||||
public side: ArenaTagSide;
|
||||
|
||||
constructor(turnCount: number, sourceMove?: MoveId, sourceId?: number, side: ArenaTagSide = ArenaTagSide.BOTH) {
|
||||
this.turnCount = turnCount;
|
||||
this.maxDuration = turnCount;
|
||||
this.sourceMove = sourceMove;
|
||||
this.sourceId = sourceId;
|
||||
this.side = side;
|
||||
@ -138,7 +145,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.
|
||||
@ -164,6 +171,7 @@ export abstract class ArenaTag implements BaseArenaTag {
|
||||
*/
|
||||
loadTag<const T extends this>(source: BaseArenaTag & Pick<T, "tagType">): void {
|
||||
this.turnCount = source.turnCount;
|
||||
this.maxDuration = source.maxDuration;
|
||||
this.sourceMove = source.sourceMove;
|
||||
this.sourceId = source.sourceId;
|
||||
this.side = source.side;
|
||||
@ -172,9 +180,8 @@ export abstract class ArenaTag implements BaseArenaTag {
|
||||
/**
|
||||
* Helper function that retrieves the source Pokemon
|
||||
* @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);
|
||||
}
|
||||
|
||||
@ -617,7 +624,7 @@ export class NoCritTag extends SerializableArenaTag {
|
||||
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:noCritOnRemove", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(source ?? undefined),
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(source),
|
||||
moveName: this.getMoveName(),
|
||||
}),
|
||||
);
|
||||
@ -1537,7 +1544,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.playActivationMessage(source);
|
||||
}
|
||||
@ -1580,7 +1587,7 @@ export class SuppressAbilitiesTag extends SerializableArenaTag {
|
||||
return this.sourceCount > 1;
|
||||
}
|
||||
|
||||
private playActivationMessage(pokemon: Pokemon | null) {
|
||||
private playActivationMessage(pokemon: Pokemon | undefined) {
|
||||
if (pokemon) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:neutralizingGasOnAdd", {
|
||||
@ -1591,6 +1598,145 @@ export class SuppressAbilitiesTag extends SerializableArenaTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface containing data related to a queued healing effect from
|
||||
* {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish}
|
||||
* or {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}.
|
||||
*/
|
||||
interface PendingHealEffect {
|
||||
/** The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} that created the effect. */
|
||||
readonly sourceId: number;
|
||||
/** The {@linkcode MoveId} of the move that created the effect. */
|
||||
readonly moveId: MoveId;
|
||||
/** If `true`, also restores the target's PP when the effect activates. */
|
||||
readonly restorePP: boolean;
|
||||
/** The message to display when the effect activates */
|
||||
readonly healMessage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Arena tag to contain stored healing effects, namely from
|
||||
* {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish}
|
||||
* and {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}.
|
||||
* When a damaged Pokemon first enters the effect's {@linkcode BattlerIndex | field position},
|
||||
* their HP is fully restored, and they are cured of any non-volatile status condition.
|
||||
* If the effect is from Lunar Dance, their PP is also restored.
|
||||
*/
|
||||
export class PendingHealTag extends SerializableArenaTag {
|
||||
public readonly tagType = ArenaTagType.PENDING_HEAL;
|
||||
/** All pending healing effects, organized by {@linkcode BattlerIndex} */
|
||||
public readonly pendingHeals: Partial<Record<BattlerIndex, PendingHealEffect[]>> = {};
|
||||
|
||||
constructor() {
|
||||
super(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a pending healing effect to the field. Effects under the same move *and*
|
||||
* target index as an existing effect are ignored.
|
||||
* @param targetIndex - The {@linkcode BattlerIndex} under which the effect applies
|
||||
* @param healEffect - The {@linkcode PendingHealEffect | data} for the pending heal effect
|
||||
*/
|
||||
public queueHeal(targetIndex: BattlerIndex, healEffect: PendingHealEffect): void {
|
||||
const existingHealEffects = this.pendingHeals[targetIndex];
|
||||
if (existingHealEffects) {
|
||||
if (!existingHealEffects.some(he => he.moveId === healEffect.moveId)) {
|
||||
existingHealEffects.push(healEffect);
|
||||
}
|
||||
} else {
|
||||
this.pendingHeals[targetIndex] = [healEffect];
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes default on-remove message */
|
||||
override onRemove(_arena: Arena): void {}
|
||||
|
||||
/** This arena tag is removed at the end of the turn if no pending healing effects are on the field */
|
||||
override lapse(_arena: Arena): boolean {
|
||||
for (const key in this.pendingHeals) {
|
||||
if (this.pendingHeals[key].length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a pending healing effect on the given target index. If an effect is found for
|
||||
* the index, the Pokemon at that index is healed to full HP, is cured of any non-volatile status,
|
||||
* and has its PP fully restored (if the effect is from Lunar Dance).
|
||||
* @param arena - The {@linkcode Arena} containing this tag
|
||||
* @param simulated - If `true`, suppresses changes to game state
|
||||
* @param pokemon - The {@linkcode Pokemon} receiving the healing effect
|
||||
* @returns `true` if the target Pokemon was healed by this effect
|
||||
* @todo This should also be called when a Pokemon moves into a new position via Ally Switch
|
||||
*/
|
||||
override apply(arena: Arena, simulated: boolean, pokemon: Pokemon): boolean {
|
||||
const targetIndex = pokemon.getBattlerIndex();
|
||||
const targetEffects = this.pendingHeals[targetIndex];
|
||||
|
||||
if (targetEffects == null || targetEffects.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const healEffect = targetEffects.find(effect => this.canApply(effect, pokemon));
|
||||
|
||||
if (healEffect == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { sourceId, moveId, restorePP, healMessage } = healEffect;
|
||||
const sourcePokemon = globalScene.getPokemonById(sourceId);
|
||||
if (!sourcePokemon) {
|
||||
console.warn(`Source of pending ${allMoves[moveId].name} effect is undefined!`);
|
||||
targetEffects.splice(targetEffects.indexOf(healEffect), 1);
|
||||
// Re-evaluate after the invalid heal effect is removed
|
||||
return this.apply(arena, simulated, pokemon);
|
||||
}
|
||||
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"PokemonHealPhase",
|
||||
targetIndex,
|
||||
pokemon.getMaxHp(),
|
||||
healMessage,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
restorePP,
|
||||
);
|
||||
|
||||
targetEffects.splice(targetEffects.indexOf(healEffect), 1);
|
||||
|
||||
return healEffect != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given {@linkcode PendingHealEffect} can immediately heal
|
||||
* the given target {@linkcode Pokemon}.
|
||||
* @param healEffect - The {@linkcode PendingHealEffect} to evaluate
|
||||
* @param pokemon - The {@linkcode Pokemon} to evaluate against
|
||||
* @returns `true` if the Pokemon can be healed by the effect
|
||||
*/
|
||||
private canApply(healEffect: PendingHealEffect, pokemon: Pokemon): boolean {
|
||||
return (
|
||||
!pokemon.isFullHp()
|
||||
|| pokemon.status != null
|
||||
|| (healEffect.restorePP && pokemon.getMoveset().some(mv => mv.ppUsed > 0))
|
||||
);
|
||||
}
|
||||
|
||||
override loadTag(source: BaseArenaTag & Pick<PendingHealTag, "tagType" | "pendingHeals">): void {
|
||||
super.loadTag(source);
|
||||
(this as Mutable<this>).pendingHeals = source.pendingHeals;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter
|
||||
export function getArenaTag(
|
||||
tagType: ArenaTagType,
|
||||
@ -1654,6 +1800,8 @@ export function getArenaTag(
|
||||
return new FairyLockTag(turnCount, sourceId);
|
||||
case ArenaTagType.NEUTRALIZING_GAS:
|
||||
return new SuppressAbilitiesTag(sourceId);
|
||||
case ArenaTagType.PENDING_HEAL:
|
||||
return new PendingHealTag();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -1702,5 +1850,6 @@ export type ArenaTagTypeMap = {
|
||||
[ArenaTagType.GRASS_WATER_PLEDGE]: GrassWaterPledgeTag;
|
||||
[ArenaTagType.FAIRY_LOCK]: FairyLockTag;
|
||||
[ArenaTagType.NEUTRALIZING_GAS]: SuppressAbilitiesTag;
|
||||
[ArenaTagType.PENDING_HEAL]: PendingHealTag;
|
||||
[ArenaTagType.NONE]: NoneTag;
|
||||
};
|
||||
|
@ -1119,7 +1119,7 @@ export const biomePokemonPools: BiomePokemonPools = {
|
||||
},
|
||||
[BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.LUCARIO, SpeciesId.THROH, SpeciesId.SAWK, { 1: [ SpeciesId.PANCHAM ], 52: [ SpeciesId.PANGORO ] } ] },
|
||||
[BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.GALAR_FARFETCHD ] },
|
||||
[BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU] }, SpeciesId.GALAR_ZAPDOS ] },
|
||||
[BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU ] }, SpeciesId.GALAR_ZAPDOS ] },
|
||||
[BiomePoolTier.BOSS]: {
|
||||
[TimeOfDay.DAWN]: [],
|
||||
[TimeOfDay.DAY]: [],
|
||||
@ -1128,7 +1128,7 @@ export const biomePokemonPools: BiomePokemonPools = {
|
||||
[TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.HARIYAMA, SpeciesId.MEDICHAM, SpeciesId.LUCARIO, SpeciesId.TOXICROAK, SpeciesId.THROH, SpeciesId.SAWK, SpeciesId.SCRAFTY, SpeciesId.MIENSHAO, SpeciesId.BEWEAR, SpeciesId.GRAPPLOCT, SpeciesId.ANNIHILAPE ]
|
||||
},
|
||||
[BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.PANGORO, SpeciesId.SIRFETCHD, SpeciesId.HISUI_DECIDUEYE ] },
|
||||
[BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU] } ] },
|
||||
[BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU ] } ] },
|
||||
[BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ZAMAZENTA, SpeciesId.GALAR_ZAPDOS ] }
|
||||
},
|
||||
[BiomeId.FACTORY]: {
|
||||
@ -1597,10 +1597,10 @@ export const biomePokemonPools: BiomePokemonPools = {
|
||||
[BiomePoolTier.UNCOMMON]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [ SpeciesId.SOLOSIS ], 32: [ SpeciesId.DUOSION ], 41: [ SpeciesId.REUNICLUS ] } ] },
|
||||
[BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.DITTO, { 1: [ SpeciesId.PORYGON ], 30: [ SpeciesId.PORYGON2 ] } ] },
|
||||
[BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM ] },
|
||||
[BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [SpeciesId.TYPE_NULL], 60: [ SpeciesId.SILVALLY ] } ] },
|
||||
[BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [ SpeciesId.TYPE_NULL ], 60: [ SpeciesId.SILVALLY ] } ] },
|
||||
[BiomePoolTier.BOSS]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MUK, SpeciesId.ELECTRODE, SpeciesId.BRONZONG, SpeciesId.MAGNEZONE, SpeciesId.PORYGON_Z, SpeciesId.REUNICLUS, SpeciesId.KLINKLANG ] },
|
||||
[BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [] },
|
||||
[BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM, SpeciesId.ZYGARDE, { 1: [SpeciesId.TYPE_NULL], 60: [ SpeciesId.SILVALLY ] } ] },
|
||||
[BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM, SpeciesId.ZYGARDE, { 1: [ SpeciesId.TYPE_NULL ], 60: [ SpeciesId.SILVALLY ] } ] },
|
||||
[BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MEWTWO, SpeciesId.MIRAIDON ] }
|
||||
},
|
||||
[BiomeId.END]: {
|
||||
@ -5627,10 +5627,12 @@ export function initBiomes() {
|
||||
]
|
||||
],
|
||||
[ SpeciesId.TYPE_NULL, PokemonType.NORMAL, -1, [
|
||||
[ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ]
|
||||
[ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ],
|
||||
[ BiomeId.LABORATORY, BiomePoolTier.BOSS_SUPER_RARE ]
|
||||
]
|
||||
],
|
||||
[ SpeciesId.SILVALLY, PokemonType.NORMAL, -1, [
|
||||
[ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ],
|
||||
[ BiomeId.LABORATORY, BiomePoolTier.BOSS_SUPER_RARE ]
|
||||
]
|
||||
],
|
||||
@ -5773,10 +5775,12 @@ export function initBiomes() {
|
||||
]
|
||||
],
|
||||
[ SpeciesId.POIPOLE, PokemonType.POISON, -1, [
|
||||
[ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ]
|
||||
[ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ],
|
||||
[ BiomeId.SWAMP, BiomePoolTier.BOSS_SUPER_RARE ]
|
||||
]
|
||||
],
|
||||
[ SpeciesId.NAGANADEL, PokemonType.POISON, PokemonType.DRAGON, [
|
||||
[ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ],
|
||||
[ BiomeId.SWAMP, BiomePoolTier.BOSS_SUPER_RARE ]
|
||||
]
|
||||
],
|
||||
@ -6165,10 +6169,12 @@ export function initBiomes() {
|
||||
]
|
||||
],
|
||||
[ SpeciesId.KUBFU, PokemonType.FIGHTING, -1, [
|
||||
[ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ]
|
||||
[ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ],
|
||||
[ BiomeId.DOJO, BiomePoolTier.BOSS_SUPER_RARE ]
|
||||
]
|
||||
],
|
||||
[ SpeciesId.URSHIFU, PokemonType.FIGHTING, PokemonType.DARK, [
|
||||
[ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ],
|
||||
[ BiomeId.DOJO, BiomePoolTier.BOSS_SUPER_RARE ]
|
||||
]
|
||||
],
|
||||
@ -7209,7 +7215,8 @@ export function initBiomes() {
|
||||
],
|
||||
[ TrainerType.SCIENTIST, [
|
||||
[ BiomeId.DESERT, BiomePoolTier.COMMON ],
|
||||
[ BiomeId.RUINS, BiomePoolTier.COMMON ]
|
||||
[ BiomeId.RUINS, BiomePoolTier.COMMON ],
|
||||
[ BiomeId.LABORATORY, BiomePoolTier.COMMON ]
|
||||
]
|
||||
],
|
||||
[ TrainerType.SMASHER, []],
|
||||
@ -7224,7 +7231,8 @@ export function initBiomes() {
|
||||
]
|
||||
],
|
||||
[ TrainerType.SWIMMER, [
|
||||
[ BiomeId.SEA, BiomePoolTier.COMMON ]
|
||||
[ BiomeId.SEA, BiomePoolTier.COMMON ],
|
||||
[ BiomeId.SEABED, BiomePoolTier.COMMON ]
|
||||
]
|
||||
],
|
||||
[ TrainerType.TWINS, [
|
||||
@ -7590,11 +7598,13 @@ export function initBiomes() {
|
||||
[ TrainerType.ALDER, []],
|
||||
[ TrainerType.IRIS, []],
|
||||
[ TrainerType.DIANTHA, []],
|
||||
[ TrainerType.KUKUI, []],
|
||||
[ TrainerType.HAU, []],
|
||||
[ TrainerType.LEON, []],
|
||||
[ TrainerType.MUSTARD, []],
|
||||
[ TrainerType.GEETA, []],
|
||||
[ TrainerType.NEMONA, []],
|
||||
[ TrainerType.KIERAN, []],
|
||||
[ TrainerType.LEON, []],
|
||||
[ TrainerType.RIVAL, []]
|
||||
];
|
||||
|
||||
|
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 type { Pokemon } from "#field/pokemon";
|
||||
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 { toCamelCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
@ -53,6 +53,7 @@ export enum EvolutionItem {
|
||||
PRISM_SCALE,
|
||||
RAZOR_CLAW,
|
||||
RAZOR_FANG,
|
||||
OVAL_STONE,
|
||||
REAPER_CLOTH,
|
||||
ELECTIRIZER,
|
||||
MAGMARIZER,
|
||||
@ -128,7 +129,7 @@ export class SpeciesEvolutionCondition {
|
||||
}
|
||||
|
||||
public get description(): string[] {
|
||||
if (!isNullOrUndefined(this.desc)) {
|
||||
if (this.desc != null) {
|
||||
return this.desc;
|
||||
}
|
||||
this.desc = this.data.map(cond => {
|
||||
@ -161,11 +162,11 @@ export class SpeciesEvolutionCondition {
|
||||
case EvoCondKey.HELD_ITEM:
|
||||
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;
|
||||
}
|
||||
|
||||
public conditionsFulfilled(pokemon: Pokemon): boolean {
|
||||
public conditionsFulfilled(pokemon: Pokemon, forFusion = false): boolean {
|
||||
console.log(this.data);
|
||||
return this.data.every(cond => {
|
||||
switch (cond.key) {
|
||||
@ -185,7 +186,7 @@ export class SpeciesEvolutionCondition {
|
||||
m.getStackCount() + pokemon.getPersistentTreasureCount() >= cond.value
|
||||
);
|
||||
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
|
||||
return false;
|
||||
case EvoCondKey.BIOME:
|
||||
@ -233,7 +234,7 @@ export class SpeciesFormEvolution {
|
||||
this.evoFormKey = evoFormKey;
|
||||
this.level = level;
|
||||
this.item = item || EvolutionItem.NONE;
|
||||
if (!isNullOrUndefined(condition)) {
|
||||
if (condition != null) {
|
||||
this.condition = new SpeciesEvolutionCondition(...coerceArray(condition));
|
||||
}
|
||||
this.wildDelay = wildDelay ?? SpeciesWildEvolutionDelay.NONE;
|
||||
@ -291,8 +292,8 @@ export class SpeciesFormEvolution {
|
||||
return (
|
||||
pokemon.level >= this.level &&
|
||||
// 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) &&
|
||||
(isNullOrUndefined(this.condition) || this.condition.conditionsFulfilled(pokemon)) &&
|
||||
(this.preFormKey == null || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) &&
|
||||
(this.condition == null || this.condition.conditionsFulfilled(pokemon, forFusion)) &&
|
||||
((item ?? EvolutionItem.NONE) === (this.item ?? EvolutionItem.NONE))
|
||||
);
|
||||
}
|
||||
@ -305,11 +306,11 @@ export class SpeciesFormEvolution {
|
||||
*/
|
||||
public isValidItemEvolution(pokemon: Pokemon, forFusion = false): boolean {
|
||||
return (
|
||||
!isNullOrUndefined(this.item) &&
|
||||
this.item != null &&
|
||||
pokemon.level >= this.level &&
|
||||
// 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) &&
|
||||
(isNullOrUndefined(this.condition) || this.condition.conditionsFulfilled(pokemon))
|
||||
(this.preFormKey == null || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) &&
|
||||
(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)
|
||||
],
|
||||
[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]: [
|
||||
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]: [
|
||||
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)
|
||||
],
|
||||
[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]: [
|
||||
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]: [
|
||||
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]: [
|
||||
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 { MoveId } from "#enums/move-id";
|
||||
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 { toKebabCase } from "#utils/strings";
|
||||
import Phaser from "phaser";
|
||||
@ -388,7 +388,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent {
|
||||
moveAnim.bgSprite.setAlpha(this.opacity / 255);
|
||||
globalScene.field.add(moveAnim.bgSprite);
|
||||
const fieldPokemon = globalScene.getEnemyPokemon(false) ?? globalScene.getPlayerPokemon(false);
|
||||
if (!isNullOrUndefined(priority)) {
|
||||
if (priority != null) {
|
||||
globalScene.field.moveTo(moveAnim.bgSprite as Phaser.GameObjects.GameObject, priority);
|
||||
} else if (fieldPokemon?.isOnField()) {
|
||||
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 encounterAnimFetches: Promise<Map<EncounterAnim, AnimConfig>>[] = [];
|
||||
for (const anim of anims) {
|
||||
if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) {
|
||||
if (encounterAnims.has(anim) && encounterAnims.get(anim) != null) {
|
||||
continue;
|
||||
}
|
||||
encounterAnimFetches.push(
|
||||
@ -1240,7 +1240,7 @@ export abstract class BattleAnim {
|
||||
|
||||
const graphicIndex = graphicFrameCount++;
|
||||
const moveSprite = sprites[graphicIndex];
|
||||
if (!isNullOrUndefined(frame.priority)) {
|
||||
if (frame.priority != null) {
|
||||
const setSpritePriority = (priority: number) => {
|
||||
if (existingFieldSprites.length > priority) {
|
||||
// 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 { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
@ -49,48 +88,9 @@ import type {
|
||||
TypeBoostTagType,
|
||||
} from "#types/battler-tags";
|
||||
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";
|
||||
|
||||
/**
|
||||
* @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 BaseBattlerTag {
|
||||
/** The tag's remaining duration */
|
||||
@ -198,7 +198,7 @@ export class BattlerTag implements BaseBattlerTag {
|
||||
* Helper function that retrieves the source Pokemon object
|
||||
* @returns The source {@linkcode Pokemon}, or `null` if none is found
|
||||
*/
|
||||
public getSourcePokemon(): Pokemon | null {
|
||||
public getSourcePokemon(): Pokemon | undefined {
|
||||
return globalScene.getPokemonById(this.sourceId);
|
||||
}
|
||||
}
|
||||
@ -378,7 +378,7 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
|
||||
// Disable fails against struggle or an empty move history
|
||||
// TODO: Confirm if this is redundant given Disable/Cursed Body's disable conditions
|
||||
const move = pokemon.getLastNonVirtualMove();
|
||||
if (isNullOrUndefined(move) || move.move === MoveId.STRUGGLE) {
|
||||
if (move == null || move.move === MoveId.STRUGGLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -451,7 +451,7 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
|
||||
override canAdd(pokemon: Pokemon): boolean {
|
||||
// Choice items ignore struggle, so Gorilla Tactics should too
|
||||
const lastSelectedMove = pokemon.getLastNonVirtualMove();
|
||||
return !isNullOrUndefined(lastSelectedMove) && lastSelectedMove.move !== MoveId.STRUGGLE;
|
||||
return lastSelectedMove != null && lastSelectedMove.move !== MoveId.STRUGGLE;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -606,17 +606,7 @@ export class ShellTrapTag extends BattlerTag {
|
||||
|
||||
// Trap should only be triggered by opponent's Physical moves
|
||||
if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) {
|
||||
const shellTrapPhaseIndex = globalScene.phaseManager.phaseQueue.findIndex(
|
||||
phase => phase.is("MovePhase") && phase.pokemon === pokemon,
|
||||
);
|
||||
const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase"));
|
||||
|
||||
// Only shift MovePhase timing if it's not already next up
|
||||
if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) {
|
||||
const shellTrapMovePhase = globalScene.phaseManager.phaseQueue.splice(shellTrapPhaseIndex, 1)[0];
|
||||
globalScene.phaseManager.prependToPhase(shellTrapMovePhase, "MovePhase");
|
||||
}
|
||||
|
||||
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === pokemon);
|
||||
this.activated = true;
|
||||
}
|
||||
|
||||
@ -968,7 +958,7 @@ export class InfatuatedTag extends SerializableBattlerTag {
|
||||
phaseManager.queueMessage(
|
||||
i18next.t("battlerTags:infatuatedLapse", {
|
||||
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);
|
||||
@ -1279,22 +1269,9 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||
}),
|
||||
);
|
||||
|
||||
const movePhase = globalScene.phaseManager.findPhase(m => m.is("MovePhase") && m.pokemon === pokemon);
|
||||
if (movePhase) {
|
||||
const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
||||
if (movesetMove) {
|
||||
const lastMove = pokemon.getLastXMoves(1)[0];
|
||||
globalScene.phaseManager.tryReplacePhase(
|
||||
m => m.is("MovePhase") && m.pokemon === pokemon,
|
||||
globalScene.phaseManager.create(
|
||||
"MovePhase",
|
||||
pokemon,
|
||||
lastMove.targets ?? [],
|
||||
movesetMove,
|
||||
MoveUseMode.NORMAL,
|
||||
),
|
||||
);
|
||||
}
|
||||
globalScene.phaseManager.changePhaseMove((phase: MovePhase) => phase.pokemon === pokemon, movesetMove);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1305,7 +1282,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (lapseType === BattlerTagLapseType.CUSTOM) {
|
||||
const encoredMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
||||
return !isNullOrUndefined(encoredMove) && encoredMove.getPpRatio() > 0;
|
||||
return encoredMove != null && encoredMove.getPpRatio() > 0;
|
||||
}
|
||||
return super.lapse(pokemon, lapseType);
|
||||
}
|
||||
@ -3578,6 +3555,25 @@ export class GrudgeTag extends SerializableBattlerTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag to allow the affected Pokemon's move to go first in its priority bracket.
|
||||
* Used for {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Draw_(Ability) | Quick Draw}
|
||||
* and {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Claw | Quick Claw}.
|
||||
*/
|
||||
export class BypassSpeedTag extends BattlerTag {
|
||||
public override readonly tagType = BattlerTagType.BYPASS_SPEED;
|
||||
|
||||
constructor() {
|
||||
super(BattlerTagType.BYPASS_SPEED, BattlerTagLapseType.TURN_END, 1);
|
||||
}
|
||||
|
||||
override canAdd(pokemon: Pokemon): boolean {
|
||||
const bypass = new BooleanHolder(true);
|
||||
applyAbAttrs("PreventBypassSpeedChanceAbAttr", { pokemon, bypass });
|
||||
return bypass.value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag used to heal the user of Psycho Shift of its status effect if Psycho Shift succeeds in transferring its status effect to the target Pokemon
|
||||
*/
|
||||
@ -3626,6 +3622,41 @@ export class MagicCoatTag extends BattlerTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag associated with {@linkcode AbilityId.SUPREME_OVERLORD}
|
||||
*/
|
||||
export class SupremeOverlordTag extends AbilityBattlerTag {
|
||||
public override readonly tagType = BattlerTagType.SUPREME_OVERLORD;
|
||||
/** The number of faints at the time the user was sent out */
|
||||
public readonly faintCount: number;
|
||||
constructor() {
|
||||
super(BattlerTagType.SUPREME_OVERLORD, AbilityId.SUPREME_OVERLORD, BattlerTagLapseType.FAINT, 0);
|
||||
}
|
||||
|
||||
public override onAdd(pokemon: Pokemon): boolean {
|
||||
(this as Mutable<this>).faintCount = Math.min(
|
||||
pokemon.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints,
|
||||
5,
|
||||
);
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("battlerTags:supremeOverlordOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The damage multiplier for Supreme Overlord
|
||||
*/
|
||||
public getBoost(): number {
|
||||
return 1 + 0.1 * this.faintCount;
|
||||
}
|
||||
|
||||
public override loadTag(source: BaseBattlerTag & Pick<SupremeOverlordTag, "tagType" | "faintCount">): void {
|
||||
super.loadTag(source);
|
||||
(this as Mutable<this>).faintCount = source.faintCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID.
|
||||
* @param sourceId - The ID of the pokemon adding the tag
|
||||
@ -3826,6 +3857,10 @@ export function getBattlerTag(
|
||||
return new PsychoShiftTag();
|
||||
case BattlerTagType.MAGIC_COAT:
|
||||
return new MagicCoatTag();
|
||||
case BattlerTagType.SUPREME_OVERLORD:
|
||||
return new SupremeOverlordTag();
|
||||
case BattlerTagType.BYPASS_SPEED:
|
||||
return new BypassSpeedTag();
|
||||
}
|
||||
}
|
||||
|
||||
@ -3960,4 +3995,6 @@ export type BattlerTagTypeMap = {
|
||||
[BattlerTagType.GRUDGE]: GrudgeTag;
|
||||
[BattlerTagType.PSYCHO_SHIFT]: PsychoShiftTag;
|
||||
[BattlerTagType.MAGIC_COAT]: MagicCoatTag;
|
||||
[BattlerTagType.SUPREME_OVERLORD]: SupremeOverlordTag;
|
||||
[BattlerTagType.BYPASS_SPEED]: BypassSpeedTag;
|
||||
};
|
||||
|
@ -6,8 +6,8 @@ import { PokemonSpecies } from "#data/pokemon-species";
|
||||
import { BiomeId } from "#enums/biome-id";
|
||||
import { PartyMemberStrength } from "#enums/party-member-strength";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import type { Starter } from "#ui/starter-select-ui-handler";
|
||||
import { isNullOrUndefined, randSeedGauss, randSeedInt, randSeedItem } from "#utils/common";
|
||||
import type { Starter } from "#types/save-data";
|
||||
import { randSeedGauss, randSeedInt, randSeedItem } from "#utils/common";
|
||||
import { getEnumValues } from "#utils/enums";
|
||||
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||
|
||||
@ -32,7 +32,7 @@ export function getDailyRunStarters(seed: string): Starter[] {
|
||||
const startingLevel = globalScene.gameMode.getStartingLevel();
|
||||
|
||||
const eventStarters = getDailyEventSeedStarters(seed);
|
||||
if (!isNullOrUndefined(eventStarters)) {
|
||||
if (eventStarters != null) {
|
||||
starters.push(...eventStarters);
|
||||
return;
|
||||
}
|
||||
@ -66,8 +66,11 @@ function getDailyRunStarter(starterSpeciesForm: PokemonSpeciesForm, startingLeve
|
||||
const formIndex = starterSpeciesForm instanceof PokemonSpecies ? undefined : starterSpeciesForm.formIndex;
|
||||
const pokemon = globalScene.addPlayerPokemon(starterSpecies, startingLevel, undefined, formIndex);
|
||||
const starter: Starter = {
|
||||
species: starterSpecies,
|
||||
dexAttr: pokemon.getDexAttr(),
|
||||
speciesId: starterSpecies.speciesId,
|
||||
shiny: pokemon.shiny,
|
||||
variant: pokemon.variant,
|
||||
formIndex: pokemon.formIndex,
|
||||
ivs: pokemon.ivs,
|
||||
abilityIndex: pokemon.abilityIndex,
|
||||
passive: false,
|
||||
nature: pokemon.getNature(),
|
||||
@ -127,7 +130,7 @@ const dailyBiomeWeights: BiomeWeights = {
|
||||
|
||||
export function getDailyStartingBiome(): BiomeId {
|
||||
const eventBiome = getDailyEventSeedBiome(globalScene.seed);
|
||||
if (!isNullOrUndefined(eventBiome)) {
|
||||
if (eventBiome != null) {
|
||||
return eventBiome;
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { PokemonType } from "#enums/pokemon-type";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||
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
|
||||
@ -78,7 +78,7 @@ export function getMoveTargets(user: Pokemon, move: MoveId, replaceTarget?: Move
|
||||
case MoveTarget.OTHER:
|
||||
case MoveTarget.ALL_NEAR_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;
|
||||
break;
|
||||
case MoveTarget.NEAR_ENEMY:
|
||||
@ -95,22 +95,22 @@ export function getMoveTargets(user: Pokemon, move: MoveId, replaceTarget?: Move
|
||||
return { targets: [-1 as BattlerIndex], multiple: false };
|
||||
case MoveTarget.NEAR_ALLY:
|
||||
case MoveTarget.ALLY:
|
||||
set = !isNullOrUndefined(ally) ? [ally] : [];
|
||||
set = ally != null ? [ally] : [];
|
||||
break;
|
||||
case MoveTarget.USER_OR_NEAR_ALLY:
|
||||
case MoveTarget.USER_AND_ALLIES:
|
||||
case MoveTarget.USER_SIDE:
|
||||
set = !isNullOrUndefined(ally) ? [user, ally] : [user];
|
||||
set = ally != null ? [user, ally] : [user];
|
||||
multiple = moveTarget !== MoveTarget.USER_OR_NEAR_ALLY;
|
||||
break;
|
||||
case MoveTarget.ALL:
|
||||
case MoveTarget.BOTH_SIDES:
|
||||
set = (!isNullOrUndefined(ally) ? [user, ally] : [user]).concat(opponents);
|
||||
set = (ally != null ? [user, ally] : [user]).concat(opponents);
|
||||
multiple = true;
|
||||
break;
|
||||
case MoveTarget.CURSE:
|
||||
{
|
||||
const extraTargets = !isNullOrUndefined(ally) ? [ally] : [];
|
||||
const extraTargets = ally != null ? [ally] : [];
|
||||
set = user.getTypes(true).includes(PokemonType.GHOST) ? opponents.concat(extraTargets) : [user];
|
||||
}
|
||||
break;
|
||||
|
@ -6,7 +6,7 @@ import { loggedInUser } from "#app/account";
|
||||
import type { GameMode } from "#app/game-mode";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import type { EntryHazardTag } from "#data/arena-tag";
|
||||
import type { EntryHazardTag, PendingHealTag } from "#data/arena-tag";
|
||||
import { WeakenMoveTypeTag } from "#data/arena-tag";
|
||||
import { MoveChargeAnim } from "#data/battle-anims";
|
||||
import {
|
||||
@ -18,6 +18,7 @@ import {
|
||||
ShellTrapTag,
|
||||
StockpilingTag,
|
||||
SubstituteTag,
|
||||
SupremeOverlordTag,
|
||||
TrappedTag,
|
||||
TypeBoostTag,
|
||||
} from "#data/battler-tags";
|
||||
@ -80,19 +81,18 @@ import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves";
|
||||
import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils";
|
||||
import { PokemonMove } from "#moves/pokemon-move";
|
||||
import { MoveEndPhase } from "#phases/move-end-phase";
|
||||
import { MovePhase } from "#phases/move-phase";
|
||||
import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
|
||||
import { SwitchSummonPhase } from "#phases/switch-summon-phase";
|
||||
import type { AttackMoveResult } from "#types/attack-move-result";
|
||||
import type { Localizable } from "#types/locales";
|
||||
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
|
||||
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 { toCamelCase, toTitleCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
import { applyChallenges } from "#utils/challenge-utils";
|
||||
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||
import type { AbstractConstructor } from "#types/type-helpers";
|
||||
|
||||
/**
|
||||
@ -835,7 +835,7 @@ export abstract class Move implements Localizable {
|
||||
|
||||
applyAbAttrs("VariableMovePowerAbAttr", abAttrParams);
|
||||
const ally = source.getAlly();
|
||||
if (!isNullOrUndefined(ally)) {
|
||||
if (ally != null) {
|
||||
applyAbAttrs("AllyMoveCategoryPowerBoostAbAttr", {...abAttrParams, pokemon: ally});
|
||||
}
|
||||
|
||||
@ -879,6 +879,8 @@ export abstract class Move implements Localizable {
|
||||
power.value *= 1.5;
|
||||
}
|
||||
|
||||
power.value *= (source.getTag(BattlerTagType.SUPREME_OVERLORD) as SupremeOverlordTag | undefined)?.getBoost() ?? 1;
|
||||
|
||||
return power.value;
|
||||
}
|
||||
|
||||
@ -888,6 +890,10 @@ export abstract class Move implements Localizable {
|
||||
applyMoveAttrs("IncrementMovePriorityAttr", user, null, this, priority);
|
||||
applyAbAttrs("ChangeMovePriorityAbAttr", {pokemon: user, simulated, move: this, priority});
|
||||
|
||||
if (user.getTag(BattlerTagType.BYPASS_SPEED)) {
|
||||
priority.value += 0.2;
|
||||
}
|
||||
|
||||
return priority.value;
|
||||
}
|
||||
|
||||
@ -965,7 +971,7 @@ export abstract class Move implements Localizable {
|
||||
|
||||
// ...and cannot enhance Pollen Puff when targeting an ally.
|
||||
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)
|
||||
&& !this.isChargingMove()
|
||||
@ -2114,7 +2120,7 @@ export class FlameBurstAttr extends MoveEffectAttr {
|
||||
const targetAlly = target.getAlly();
|
||||
const cancelled = new BooleanHolder(false);
|
||||
|
||||
if (!isNullOrUndefined(targetAlly)) {
|
||||
if (targetAlly != null) {
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: targetAlly, cancelled});
|
||||
}
|
||||
|
||||
@ -2127,7 +2133,7 @@ export class FlameBurstAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||
return !isNullOrUndefined(target.getAlly()) ? -5 : 0;
|
||||
return target.getAlly() != null ? -5 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2147,24 +2153,15 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We don't know which party member will be chosen, so pick the highest max HP in the party
|
||||
const party = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
||||
const maxPartyMemberHp = party.map(p => p.getMaxHp()).reduce((maxHp: number, hp: number) => Math.max(hp, maxHp), 0);
|
||||
|
||||
const pm = globalScene.phaseManager;
|
||||
|
||||
pm.pushPhase(
|
||||
pm.create("PokemonHealPhase",
|
||||
user.getBattlerIndex(),
|
||||
maxPartyMemberHp,
|
||||
i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
this.restorePP),
|
||||
true);
|
||||
// Add a tag to the field if it doesn't already exist, then queue a delayed healing effect in the user's current slot.
|
||||
globalScene.arena.addTag(ArenaTagType.PENDING_HEAL, 0, move.id, user.id); // Arguments after first go completely unused
|
||||
const tag = globalScene.arena.getTag(ArenaTagType.PENDING_HEAL) as PendingHealTag;
|
||||
tag.queueHeal(user.getBattlerIndex(), {
|
||||
sourceId: user.id,
|
||||
moveId: move.id,
|
||||
restorePP: this.restorePP,
|
||||
healMessage: i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }),
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -3156,7 +3153,7 @@ export class WeatherInstantChargeAttr extends InstantChargeAttr {
|
||||
super((user, move) => {
|
||||
const currentWeather = globalScene.arena.weather;
|
||||
|
||||
if (isNullOrUndefined(currentWeather?.weatherType)) {
|
||||
if (currentWeather?.weatherType == null) {
|
||||
return false;
|
||||
} else {
|
||||
return !currentWeather?.isEffectSuppressed()
|
||||
@ -3304,7 +3301,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
|
||||
|
||||
const overridden = args[0] as BooleanHolder;
|
||||
|
||||
const allyMovePhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => phase.is("MovePhase") && phase.pokemon.isPlayer() === user.isPlayer());
|
||||
const allyMovePhase = globalScene.phaseManager.getMovePhase((phase) => phase.pokemon.isPlayer() === user.isPlayer());
|
||||
if (allyMovePhase) {
|
||||
const allyMove = allyMovePhase.move.getMove();
|
||||
if (allyMove !== move && allyMove.hasAttr("AwaitCombinedPledgeAttr")) {
|
||||
@ -3317,11 +3314,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
|
||||
}));
|
||||
|
||||
// Move the ally's MovePhase (if needed) so that the ally moves next
|
||||
const allyMovePhaseIndex = globalScene.phaseManager.phaseQueue.indexOf(allyMovePhase);
|
||||
const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex((phase) => phase.is("MovePhase"));
|
||||
if (allyMovePhaseIndex !== firstMovePhaseIndex) {
|
||||
globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(allyMovePhaseIndex, 1)[0], "MovePhase");
|
||||
}
|
||||
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === user.getAlly());
|
||||
|
||||
overridden.value = true;
|
||||
return true;
|
||||
@ -4556,28 +4549,7 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr {
|
||||
*/
|
||||
apply(user: Pokemon, _target: Pokemon, _move: Move, args: any[]): boolean {
|
||||
const power = args[0] as NumberHolder;
|
||||
const enemy = user.getOpponent(0);
|
||||
const pokemonActed: Pokemon[] = [];
|
||||
|
||||
if (enemy?.turnData.acted) {
|
||||
pokemonActed.push(enemy);
|
||||
}
|
||||
|
||||
if (globalScene.currentBattle.double) {
|
||||
const userAlly = user.getAlly();
|
||||
const enemyAlly = enemy?.getAlly();
|
||||
|
||||
if (userAlly?.turnData.acted) {
|
||||
pokemonActed.push(userAlly);
|
||||
}
|
||||
if (enemyAlly?.turnData.acted) {
|
||||
pokemonActed.push(enemyAlly);
|
||||
}
|
||||
}
|
||||
|
||||
pokemonActed.sort((a, b) => b.turnData.order - a.turnData.order);
|
||||
|
||||
for (const p of pokemonActed) {
|
||||
for (const p of globalScene.phaseManager.dynamicQueueManager.getLastTurnOrder().slice(0, -1).reverse()) {
|
||||
const [ lastMove ] = p.getLastXMoves(1);
|
||||
if (lastMove.result !== MoveResult.FAIL) {
|
||||
if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) {
|
||||
@ -4659,20 +4631,13 @@ export class CueNextRoundAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
|
||||
const nextRoundPhase = globalScene.phaseManager.findPhase<MovePhase>(phase =>
|
||||
phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND
|
||||
);
|
||||
const nextRoundPhase = globalScene.phaseManager.getMovePhase(phase => phase.move.moveId === MoveId.ROUND);
|
||||
|
||||
if (!nextRoundPhase) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update the phase queue so that the next Pokemon using Round moves next
|
||||
const nextRoundIndex = globalScene.phaseManager.phaseQueue.indexOf(nextRoundPhase);
|
||||
const nextMoveIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase"));
|
||||
if (nextRoundIndex !== nextMoveIndex) {
|
||||
globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(nextRoundIndex, 1)[0], "MovePhase");
|
||||
}
|
||||
globalScene.phaseManager.forceMoveNext(phase => phase.move.moveId === MoveId.ROUND);
|
||||
|
||||
// Mark the corresponding Pokemon as having "joined the Round" (for doubling power later)
|
||||
nextRoundPhase.pokemon.turnData.joinedRound = true;
|
||||
@ -6293,15 +6258,15 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
|
||||
pokemon.heal(Math.min(toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp()));
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: getPokemonNameWithAffix(pokemon) }), 0, true);
|
||||
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
|
||||
if (allyPokemon.isFainted() || allyPokemon === pokemon) {
|
||||
// Enemy switch phase should be removed and replaced with the revived pkmn switching in
|
||||
globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("SwitchSummonPhase") && phase.getPokemon() === pokemon);
|
||||
globalScene.phaseManager.tryRemovePhase("SwitchSummonPhase", phase => phase.getFieldIndex() === slotIndex);
|
||||
// If the pokemon being revived was alive earlier in the turn, cancel its move
|
||||
// (revived pokemon can't move in the turn they're brought back)
|
||||
// TODO: might make sense to move this to `FaintPhase` after checking for Rev Seed (rather than handling it in the move)
|
||||
globalScene.phaseManager.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
|
||||
globalScene.phaseManager.getMovePhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
|
||||
if (user.fieldPosition === FieldPosition.CENTER) {
|
||||
user.setFieldPosition(FieldPosition.LEFT);
|
||||
}
|
||||
@ -6382,8 +6347,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||
switchOutTarget.leaveField(true);
|
||||
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
|
||||
globalScene.phaseManager.prependNewToPhase(
|
||||
"MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchSummonPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6393,7 +6357,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
);
|
||||
} else {
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6422,7 +6386,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||
switchOutTarget.leaveField(true);
|
||||
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
|
||||
globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchSummonPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6432,7 +6396,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
);
|
||||
} else {
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchSummonPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6462,7 +6426,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@ -6863,7 +6827,7 @@ class CallMoveAttr extends OverrideMoveEffectAttr {
|
||||
: moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]];
|
||||
|
||||
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -7095,7 +7059,7 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr {
|
||||
|
||||
// Load the move's animation if we didn't already and unshift a new usage phase
|
||||
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -7122,7 +7086,7 @@ export class CopyMoveAttr extends CallMoveAttr {
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (_user, target, _move) => {
|
||||
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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -7169,7 +7133,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
||||
&& firstTarget !== target.getAlly()
|
||||
) {
|
||||
const ally = firstTarget.getAlly();
|
||||
if (!isNullOrUndefined(ally) && ally.isActive()) {
|
||||
if (ally != null && ally.isActive()) {
|
||||
moveTargets = [ ally.getBattlerIndex() ];
|
||||
}
|
||||
}
|
||||
@ -7179,7 +7143,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
||||
targetPokemonName: getPokemonNameWithAffix(target)
|
||||
}));
|
||||
target.turnData.extraTurns++;
|
||||
globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL, MovePhaseTimingModifier.FIRST);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -7478,7 +7442,7 @@ export class SketchAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
const targetMove = target.getLastNonVirtualMove();
|
||||
return !isNullOrUndefined(targetMove)
|
||||
return targetMove != null
|
||||
&& !invalidSketchMoves.has(targetMove.move)
|
||||
&& user.getMoveset().every(m => m.moveId !== targetMove.move)
|
||||
};
|
||||
@ -7535,7 +7499,7 @@ export class AbilityCopyAttr extends MoveEffectAttr {
|
||||
user.setTempAbility(target.getAbility());
|
||||
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 }));
|
||||
ally.setTempAbility(target.getAbility());
|
||||
}
|
||||
@ -7954,12 +7918,7 @@ export class AfterYouAttr extends MoveEffectAttr {
|
||||
*/
|
||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) }));
|
||||
|
||||
// Will find next acting phase of the targeted pokémon, delete it and queue it right after us.
|
||||
const targetNextPhase = globalScene.phaseManager.findPhase<MovePhase>(phase => phase.pokemon === target);
|
||||
if (targetNextPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
|
||||
globalScene.phaseManager.prependToPhase(targetNextPhase, "MovePhase");
|
||||
}
|
||||
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === target);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -7982,45 +7941,11 @@ export class ForceLastAttr extends MoveEffectAttr {
|
||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
|
||||
|
||||
// TODO: Refactor this to be more readable and less janky
|
||||
const targetMovePhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => phase.pokemon === target);
|
||||
if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
|
||||
// Finding the phase to insert the move in front of -
|
||||
// Either the end of the turn or in front of another, slower move which has also been forced last
|
||||
const prependPhase = globalScene.phaseManager.findPhase((phase) =>
|
||||
[ MovePhase, MoveEndPhase ].every(cls => !(phase instanceof cls))
|
||||
|| (phase.is("MovePhase")) && phaseForcedSlower(phase, target, !!globalScene.arena.getTag(ArenaTagType.TRICK_ROOM))
|
||||
);
|
||||
if (prependPhase) {
|
||||
globalScene.phaseManager.phaseQueue.splice(
|
||||
globalScene.phaseManager.phaseQueue.indexOf(prependPhase),
|
||||
0,
|
||||
globalScene.phaseManager.create("MovePhase", target, [ ...targetMovePhase.targets ], targetMovePhase.move, targetMovePhase.useMode, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
globalScene.phaseManager.forceMoveLast((phase: MovePhase) => phase.pokemon === target);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target}.
|
||||
|
||||
* TODO:
|
||||
- Make this a class method
|
||||
- Make this look at speed order from TurnStartPhase
|
||||
*/
|
||||
const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => {
|
||||
let slower: boolean;
|
||||
// quashed pokemon still have speed ties
|
||||
if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) {
|
||||
slower = !!target.randBattleSeedInt(2);
|
||||
} else {
|
||||
slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD);
|
||||
}
|
||||
return phase.isForcedLast() && slower;
|
||||
};
|
||||
|
||||
const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
|
||||
|
||||
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
|
||||
@ -8044,7 +7969,7 @@ const userSleptOrComatoseCondition: MoveConditionFunc = (user) => user.status?.e
|
||||
|
||||
const targetSleptOrComatoseCondition: MoveConditionFunc = (_user: Pokemon, target: Pokemon, _move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
|
||||
|
||||
const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.findPhase(phase => phase.is("MovePhase")) !== undefined;
|
||||
const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.hasPhaseOfType("MovePhase");
|
||||
|
||||
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
||||
@ -8056,7 +7981,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 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 "";
|
||||
}
|
||||
const heldItems = target.getHeldItems().filter(i => i.isTransferable);
|
||||
@ -8613,7 +8538,7 @@ export function initMoves() {
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true)
|
||||
.condition((_user, target, _move) => {
|
||||
const lastNonVirtualMove = target.getLastNonVirtualMove();
|
||||
return !isNullOrUndefined(lastNonVirtualMove) && lastNonVirtualMove.move !== MoveId.STRUGGLE;
|
||||
return lastNonVirtualMove != null && lastNonVirtualMove.move !== MoveId.STRUGGLE;
|
||||
})
|
||||
.ignoresSubstitute()
|
||||
.reflectable(),
|
||||
@ -9957,7 +9882,7 @@ export function initMoves() {
|
||||
.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) => !(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.FLOATING, false, true, 3)
|
||||
.reflectable(),
|
||||
|
@ -48,7 +48,7 @@ import { getRandomPartyMemberFunc, trainerConfigs } from "#trainers/trainer-conf
|
||||
import { TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#trainers/trainer-party-template";
|
||||
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
||||
import { MoveInfoOverlay } from "#ui/move-info-overlay";
|
||||
import { isNullOrUndefined, randSeedInt, randSeedShuffle } from "#utils/common";
|
||||
import { randSeedInt, randSeedShuffle } from "#utils/common";
|
||||
import i18next from "i18next";
|
||||
|
||||
/** the i18n namespace for the encounter */
|
||||
@ -571,7 +571,7 @@ function getTrainerConfigForWave(waveIndex: number) {
|
||||
.setPartyMemberFunc(
|
||||
4,
|
||||
getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => {
|
||||
if (!isNullOrUndefined(pool3Mon.formIndex)) {
|
||||
if (pool3Mon.formIndex != null) {
|
||||
p.formIndex = pool3Mon.formIndex;
|
||||
p.generateAndPopulateMoveset();
|
||||
p.generateName();
|
||||
@ -603,7 +603,7 @@ function getTrainerConfigForWave(waveIndex: number) {
|
||||
.setPartyMemberFunc(
|
||||
3,
|
||||
getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => {
|
||||
if (!isNullOrUndefined(pool3Mon.formIndex)) {
|
||||
if (pool3Mon.formIndex != null) {
|
||||
p.formIndex = pool3Mon.formIndex;
|
||||
p.generateAndPopulateMoveset();
|
||||
p.generateName();
|
||||
@ -613,7 +613,7 @@ function getTrainerConfigForWave(waveIndex: number) {
|
||||
.setPartyMemberFunc(
|
||||
4,
|
||||
getRandomPartyMemberFunc([pool3Mon2.species], TrainerSlot.TRAINER, true, p => {
|
||||
if (!isNullOrUndefined(pool3Mon2.formIndex)) {
|
||||
if (pool3Mon2.formIndex != null) {
|
||||
p.formIndex = pool3Mon2.formIndex;
|
||||
p.generateAndPopulateMoveset();
|
||||
p.generateName();
|
||||
@ -648,7 +648,7 @@ function getTrainerConfigForWave(waveIndex: number) {
|
||||
.setPartyMemberFunc(
|
||||
3,
|
||||
getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => {
|
||||
if (!isNullOrUndefined(pool3Mon.formIndex)) {
|
||||
if (pool3Mon.formIndex != null) {
|
||||
p.formIndex = pool3Mon.formIndex;
|
||||
p.generateAndPopulateMoveset();
|
||||
p.generateName();
|
||||
@ -687,7 +687,7 @@ function getTrainerConfigForWave(waveIndex: number) {
|
||||
.setPartyMemberFunc(
|
||||
2,
|
||||
getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => {
|
||||
if (!isNullOrUndefined(pool3Mon.formIndex)) {
|
||||
if (pool3Mon.formIndex != null) {
|
||||
p.formIndex = pool3Mon.formIndex;
|
||||
p.generateAndPopulateMoveset();
|
||||
p.generateName();
|
||||
@ -697,7 +697,7 @@ function getTrainerConfigForWave(waveIndex: number) {
|
||||
.setPartyMemberFunc(
|
||||
3,
|
||||
getRandomPartyMemberFunc([pool3Mon2.species], TrainerSlot.TRAINER, true, p => {
|
||||
if (!isNullOrUndefined(pool3Mon2.formIndex)) {
|
||||
if (pool3Mon2.formIndex != null) {
|
||||
p.formIndex = pool3Mon2.formIndex;
|
||||
p.generateAndPopulateMoveset();
|
||||
p.generateName();
|
||||
|
@ -15,7 +15,7 @@ import { getRandomPlayerPokemon, getRandomSpeciesByStarterCost } from "#mystery-
|
||||
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
|
||||
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
|
||||
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";
|
||||
|
||||
/** 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;
|
||||
}
|
||||
const config: EnemyPartyConfig = {
|
||||
|