Merge branch 'beta' into localization-md

This commit is contained in:
Bertie690 2025-06-28 14:22:00 +01:00 committed by GitHub
commit de31b0d5d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 677 additions and 415 deletions

View File

@ -2,7 +2,7 @@
Thank you for taking the time to contribute, every little bit helps. This project is entirely open-source and unmonetized - community contributions are what keep it alive! Thank you for taking the time to contribute, every little bit helps. This project is entirely open-source and unmonetized - community contributions are what keep it alive!
Please make sure you understand everything relevant to your changes from the [Table of Contents](#-table-of-contents), and *feel free to reach out reach out in the **#dev-corner** channel on [Discord](https://discord.gg/pokerogue)*. Please make sure you understand everything relevant to your changes from the [Table of Contents](#-table-of-contents), and absolutely *feel free to reach out in the **#dev-corner** channel on [Discord](https://discord.gg/pokerogue)*.
We are here to help and the better you understand what you're working on, the easier it will be for it to find its way into the game. We are here to help and the better you understand what you're working on, the easier it will be for it to find its way into the game.
## 📄 Table of Contents ## 📄 Table of Contents
@ -36,10 +36,6 @@ If you have the motivation and experience with Typescript/Javascript (or are wil
[^1]: If you forget to include the `--recurse-submodules` flag when cloning initially (or do so via an alternate tool), consult [localization.md](./docs/localization.md) \ [^1]: If you forget to include the `--recurse-submodules` flag when cloning initially (or do so via an alternate tool), consult [localization.md](./docs/localization.md) \
for instructions on how to clone the `locales` submodule manually. for instructions on how to clone the `locales` submodule manually.
### Linting
Check out our [in-depth file](./docs/linting.md) on linting and formatting!
## 🚀 Getting Started ## 🚀 Getting Started
A great way to develop an understanding of how the project works is to look at test cases (located in [the `test` folder](./test/)). A great way to develop an understanding of how the project works is to look at test cases (located in [the `test` folder](./test/)).

View File

@ -70,7 +70,10 @@
}, },
"style": { "style": {
"useEnumInitializers": "off", // large enums like Moves/Species would make this cumbersome "useEnumInitializers": "off", // large enums like Moves/Species would make this cumbersome
"useBlockStatements": "error", "useBlockStatements": {
"level": "error",
"fix": "safe"
},
"useConst": "error", "useConst": "error",
"useImportType": "error", "useImportType": "error",
"noNonNullAssertion": "off", // TODO: Turn this on ASAP and fix all non-null assertions in non-test files "noNonNullAssertion": "off", // TODO: Turn this on ASAP and fix all non-null assertions in non-test files

View File

@ -0,0 +1,12 @@
import type { BattlerIndex } from "#enums/battler-index";
import type { DamageResult } from "#app/@types/damage-result";
import type { MoveId } from "#enums/move-id";
export interface AttackMoveResult {
move: MoveId;
result: DamageResult;
damage: number;
critical: boolean;
sourceId: number;
sourceBattlerIndex: BattlerIndex;
}

View File

@ -0,0 +1,21 @@
import type { HitResult } from "#enums/hit-result";
/** Union type containing all damage-dealing {@linkcode HitResult}s. */
export type DamageResult =
| HitResult.EFFECTIVE
| HitResult.SUPER_EFFECTIVE
| HitResult.NOT_VERY_EFFECTIVE
| HitResult.ONE_HIT_KO
| HitResult.CONFUSION
| HitResult.INDIRECT_KO
| HitResult.INDIRECT;
/** Interface containing the results of a damage calculation for a given move. */
export interface DamageCalculationResult {
/** `true` if the move was cancelled (thus suppressing "No Effect" messages) */
cancelled: boolean;
/** The effectiveness of the move */
result: HitResult;
/** The damage dealt by the move */
damage: number;
}

View File

@ -0,0 +1,41 @@
import type { Gender } from "#app/data/gender";
import type PokemonSpecies from "#app/data/pokemon-species";
import type { Variant } from "#app/sprites/variant";
import type { PokeballType } from "#enums/pokeball";
import type { SpeciesId } from "#enums/species-id";
/**
* Data pertaining to a Pokemon's Illusion.
*/
export interface IllusionData {
basePokemon: {
/** The actual name of the Pokemon */
name: string;
/** The actual nickname of the Pokemon */
nickname: string;
/** Whether the base pokemon is shiny or not */
shiny: boolean;
/** The shiny variant of the base pokemon */
variant: Variant;
/** Whether the fusion species of the base pokemon is shiny or not */
fusionShiny: boolean;
/** The variant of the fusion species of the base pokemon */
fusionVariant: Variant;
};
/** The species of the illusion */
species: SpeciesId;
/** The formIndex of the illusion */
formIndex: number;
/** The gender of the illusion */
gender: Gender;
/** The pokeball of the illusion */
pokeball: PokeballType;
/** The fusion species of the illusion if it's a fusion */
fusionSpecies?: PokemonSpecies;
/** The fusionFormIndex of the illusion */
fusionFormIndex?: number;
/** The fusionGender of the illusion if it's a fusion */
fusionGender?: Gender;
/** The level of the illusion (not used currently) */
level?: number;
}

13
src/@types/turn-move.ts Normal file
View File

@ -0,0 +1,13 @@
import type { BattlerIndex } from "#enums/battler-index";
import type { MoveId } from "#enums/move-id";
import type { MoveResult } from "#enums/move-result";
import type { MoveUseMode } from "#enums/move-use-mode";
/** A record of a move having been used. */
export interface TurnMove {
move: MoveId;
targets: BattlerIndex[];
useMode: MoveUseMode;
result?: MoveResult;
turn?: number;
}

View File

@ -18,6 +18,7 @@ import {
isNullOrUndefined, isNullOrUndefined,
BooleanHolder, BooleanHolder,
type Constructor, type Constructor,
isBetween,
} from "#app/utils/common"; } from "#app/utils/common";
import { deepMergeSpriteData } from "#app/utils/data"; import { deepMergeSpriteData } from "#app/utils/data";
import type { Modifier, ModifierPredicate, TurnHeldItemTransferModifier } from "./modifier/modifier"; import type { Modifier, ModifierPredicate, TurnHeldItemTransferModifier } from "./modifier/modifier";
@ -164,10 +165,6 @@ import { PhaseManager } from "./phase-manager";
const DEBUG_RNG = false; const DEBUG_RNG = false;
const OPP_IVS_OVERRIDE_VALIDATED: number[] = (
Array.isArray(Overrides.OPP_IVS_OVERRIDE) ? Overrides.OPP_IVS_OVERRIDE : new Array(6).fill(Overrides.OPP_IVS_OVERRIDE)
).map(iv => (Number.isNaN(iv) || iv === null || iv > 31 ? -1 : iv));
export interface PokeballCounts { export interface PokeballCounts {
[pb: string]: number; [pb: string]: number;
} }
@ -934,9 +931,32 @@ export default class BattleScene extends SceneBase {
nature, nature,
dataSource, dataSource,
); );
if (postProcess) { if (postProcess) {
postProcess(pokemon); postProcess(pokemon);
} }
if (Overrides.IVS_OVERRIDE === null) {
// do nothing
} else if (Array.isArray(Overrides.IVS_OVERRIDE)) {
if (Overrides.IVS_OVERRIDE.length !== 6) {
throw new Error("The Player IVs override must be an array of length 6 or a number!");
}
if (Overrides.IVS_OVERRIDE.some(value => !isBetween(value, 0, 31))) {
throw new Error("All IVs in the player IV override must be between 0 and 31!");
}
pokemon.ivs = Overrides.IVS_OVERRIDE;
} else {
if (!isBetween(Overrides.IVS_OVERRIDE, 0, 31)) {
throw new Error("The Player IV override must be a value between 0 and 31!");
}
pokemon.ivs = new Array(6).fill(Overrides.IVS_OVERRIDE);
}
if (Overrides.NATURE_OVERRIDE !== null) {
pokemon.nature = Overrides.NATURE_OVERRIDE;
}
pokemon.init(); pokemon.init();
return pokemon; return pokemon;
} }
@ -981,10 +1001,25 @@ export default class BattleScene extends SceneBase {
postProcess(pokemon); postProcess(pokemon);
} }
for (let i = 0; i < pokemon.ivs.length; i++) { if (Overrides.ENEMY_IVS_OVERRIDE === null) {
if (OPP_IVS_OVERRIDE_VALIDATED[i] > -1) { // do nothing
pokemon.ivs[i] = OPP_IVS_OVERRIDE_VALIDATED[i]; } else if (Array.isArray(Overrides.ENEMY_IVS_OVERRIDE)) {
if (Overrides.ENEMY_IVS_OVERRIDE.length !== 6) {
throw new Error("The Enemy IVs override must be an array of length 6 or a number!");
} }
if (Overrides.ENEMY_IVS_OVERRIDE.some(value => !isBetween(value, 0, 31))) {
throw new Error("All IVs in the enemy IV override must be between 0 and 31!");
}
pokemon.ivs = Overrides.ENEMY_IVS_OVERRIDE;
} else {
if (!isBetween(Overrides.ENEMY_IVS_OVERRIDE, 0, 31)) {
throw new Error("The Enemy IV override must be a value between 0 and 31!");
}
pokemon.ivs = new Array(6).fill(Overrides.ENEMY_IVS_OVERRIDE);
}
if (Overrides.ENEMY_NATURE_OVERRIDE !== null) {
pokemon.nature = Overrides.ENEMY_NATURE_OVERRIDE;
} }
pokemon.init(); pokemon.init();

View File

@ -17,7 +17,8 @@ import { MoneyMultiplierModifier, type PokemonHeldItemModifier } from "./modifie
import type { PokeballType } from "#enums/pokeball"; import type { PokeballType } from "#enums/pokeball";
import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { trainerConfigs } from "#app/data/trainers/trainer-config";
import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesFormKey } from "#enums/species-form-key";
import type { EnemyPokemon, PlayerPokemon, TurnMove } from "#app/field/pokemon"; import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import type { TurnMove } from "./@types/turn-move";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { BattleSpec } from "#enums/battle-spec"; import { BattleSpec } from "#enums/battle-spec";

View File

@ -673,7 +673,12 @@ export class ConfusedTag extends BattlerTag {
} }
canAdd(pokemon: Pokemon): boolean { canAdd(pokemon: Pokemon): boolean {
return globalScene.arena.terrain?.terrainType !== TerrainType.MISTY || !pokemon.isGrounded(); const blockedByTerrain = pokemon.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.MISTY;
if (blockedByTerrain) {
pokemon.queueStatusImmuneMessage(false, TerrainType.MISTY);
return false;
}
return true;
} }
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {

View File

@ -1,31 +0,0 @@
import type { AbilityId } from "#enums/ability-id";
import type { PokemonType } from "#enums/pokemon-type";
import type { Nature } from "#enums/nature";
/**
* Data that can customize a Pokemon in non-standard ways from its Species.
* Includes abilities, nature, changed types, etc.
*/
export class CustomPokemonData {
// TODO: Change the default value for all these from -1 to something a bit more sensible
/**
* The scale at which to render this Pokemon's sprite.
*/
public spriteScale = -1;
public ability: AbilityId | -1;
public passive: AbilityId | -1;
public nature: Nature | -1;
public types: PokemonType[];
/** Deprecated but needed for session save migration */
// TODO: Remove this once pre-session migration is implemented
public hitsRecCount: number | null = null;
constructor(data?: CustomPokemonData | Partial<CustomPokemonData>) {
this.spriteScale = data?.spriteScale ?? -1;
this.ability = data?.ability ?? -1;
this.passive = data?.passive ?? -1;
this.nature = data?.nature ?? -1;
this.types = data?.types ?? [];
this.hitsRecCount = data?.hitsRecCount ?? null;
}
}

View File

@ -294,7 +294,7 @@ export class Egg {
public getEggDescriptor(): string { public getEggDescriptor(): string {
if (this.isManaphyEgg()) { if (this.isManaphyEgg()) {
return "Manaphy"; return i18next.t("egg:manaphyTier");
} }
switch (this.tier) { switch (this.tier) {
case EggTier.RARE: case EggTier.RARE:

View File

@ -13,7 +13,8 @@ import {
TypeBoostTag, TypeBoostTag,
} from "../battler-tags"; } from "../battler-tags";
import { getPokemonNameWithAffix } from "../../messages"; import { getPokemonNameWithAffix } from "../../messages";
import type { AttackMoveResult, TurnMove } from "../../field/pokemon"; import type { TurnMove } from "#app/@types/turn-move";
import type { AttackMoveResult } from "#app/@types/attack-move-result";
import type Pokemon from "../../field/pokemon"; import type Pokemon from "../../field/pokemon";
import type { EnemyPokemon } from "#app/field/pokemon"; import type { EnemyPokemon } from "#app/field/pokemon";
import { PokemonMove } from "./pokemon-move"; import { PokemonMove } from "./pokemon-move";

View File

@ -44,7 +44,7 @@ import { BattlerIndex } from "#enums/battler-index";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { EncounterBattleAnim } from "#app/data/battle-anims"; import { EncounterBattleAnim } from "#app/data/battle-anims";
import { MoveCategory } from "#enums/MoveCategory"; import { MoveCategory } from "#enums/MoveCategory";
import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CustomPokemonData } from "#app/data/pokemon/pokemon-data";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { EncounterAnim } from "#enums/encounter-anims"; import { EncounterAnim } from "#enums/encounter-anims";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";

View File

@ -29,7 +29,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CustomPokemonData } from "#app/data/pokemon/pokemon-data";
import { randSeedInt } from "#app/utils/common"; import { randSeedInt } from "#app/utils/common";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";

View File

@ -25,7 +25,7 @@ import { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CustomPokemonData } from "#app/data/pokemon/pokemon-data";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";

View File

@ -43,7 +43,7 @@ import { TrainerSlot } from "#enums/trainer-slot";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import type { IEggOptions } from "#app/data/egg"; import type { IEggOptions } from "#app/data/egg";
import { Egg } from "#app/data/egg"; import { Egg } from "#app/data/egg";
import type { CustomPokemonData } from "#app/data/custom-pokemon-data"; import type { CustomPokemonData } from "#app/data/pokemon/pokemon-data";
import type HeldModifierConfig from "#app/@types/held-modifier-config"; import type HeldModifierConfig from "#app/@types/held-modifier-config";
import type { Variant } from "#app/sprites/variant"; import type { Variant } from "#app/sprites/variant";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";

View File

@ -33,7 +33,7 @@ import { modifierTypes } from "#app/data/data-lists";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import type { PermanentStat } from "#enums/stat"; import type { PermanentStat } from "#enums/stat";
import { SummaryUiMode } from "#app/ui/summary-ui-handler"; import { SummaryUiMode } from "#app/ui/summary-ui-handler";
import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CustomPokemonData } from "#app/data/pokemon/pokemon-data";
import type { AbilityId } from "#enums/ability-id"; import type { AbilityId } from "#enums/ability-id";
import type { PokeballType } from "#enums/pokeball"; import type { PokeballType } from "#enums/pokeball";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";

View File

@ -0,0 +1,208 @@
import { type BattlerTag, loadBattlerTag } from "#app/data/battler-tags";
import type { Gender } from "#app/data/gender";
import type { PokemonSpeciesForm } from "#app/data/pokemon-species";
import type { TypeDamageMultiplier } from "#app/data/type";
import { isNullOrUndefined } from "#app/utils/common";
import type { AbilityId } from "#enums/ability-id";
import type { BerryType } from "#enums/berry-type";
import type { MoveId } from "#enums/move-id";
import type { PokemonType } from "#enums/pokemon-type";
import { PokemonMove } from "#app/data/moves/pokemon-move";
import type { TurnMove } from "#app/@types/turn-move";
import type { AttackMoveResult } from "#app/@types/attack-move-result";
import type { Nature } from "#enums/nature";
import type { IllusionData } from "#app/@types/illusion-data";
/**
* Permanent data that can customize a Pokemon in non-standard ways from its Species.
* Includes abilities, nature, changed types, etc.
*/
export class CustomPokemonData {
// TODO: Change the default value for all these from -1 to something a bit more sensible
/**
* The scale at which to render this Pokemon's sprite.
*/
public spriteScale = -1;
public ability: AbilityId | -1;
public passive: AbilityId | -1;
public nature: Nature | -1;
public types: PokemonType[];
/** Deprecated but needed for session save migration */
// TODO: Remove this once pre-session migration is implemented
public hitsRecCount: number | null = null;
constructor(data?: CustomPokemonData | Partial<CustomPokemonData>) {
this.spriteScale = data?.spriteScale ?? -1;
this.ability = data?.ability ?? -1;
this.passive = data?.passive ?? -1;
this.nature = data?.nature ?? -1;
this.types = data?.types ?? [];
this.hitsRecCount = data?.hitsRecCount ?? null;
}
}
/**
* Persistent in-battle data for a {@linkcode Pokemon}.
* Resets on switch or new battle.
*/
export class PokemonSummonData {
/** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */
public statStages: number[] = [0, 0, 0, 0, 0, 0, 0];
/**
* A queue of moves yet to be executed, used by charging, recharging and frenzy moves.
* So long as this array is nonempty, this Pokemon's corresponding `CommandPhase` will be skipped over entirely
* in favor of using the queued move.
* TODO: Clean up a lot of the code surrounding the move queue.
*/
public moveQueue: TurnMove[] = [];
public tags: BattlerTag[] = [];
public abilitySuppressed = false;
// Overrides for transform.
// TODO: Move these into a separate class & add rage fist hit count
public speciesForm: PokemonSpeciesForm | null = null;
public fusionSpeciesForm: PokemonSpeciesForm | null = null;
public ability: AbilityId | undefined;
public passiveAbility: AbilityId | undefined;
public gender: Gender | undefined;
public fusionGender: Gender | undefined;
public stats: number[] = [0, 0, 0, 0, 0, 0];
public moveset: PokemonMove[] | null;
// If not initialized this value will not be populated from save data.
public types: PokemonType[] = [];
public addedType: PokemonType | null = null;
/** Data pertaining to this pokemon's illusion. */
public illusion: IllusionData | null = null;
public illusionBroken = false;
/** Array containing all berries eaten in the last turn; used by {@linkcode AbilityId.CUD_CHEW} */
public berriesEatenLast: BerryType[] = [];
/**
* An array of all moves this pokemon has used since entering the battle.
* Used for most moves and abilities that check prior move usage or copy already-used moves.
*/
public moveHistory: TurnMove[] = [];
constructor(source?: PokemonSummonData | Partial<PokemonSummonData>) {
if (isNullOrUndefined(source)) {
return;
}
// TODO: Rework this into an actual generic function for use elsewhere
for (const [key, value] of Object.entries(source)) {
if (isNullOrUndefined(value) && this.hasOwnProperty(key)) {
continue;
}
if (key === "moveset") {
this.moveset = value?.map((m: any) => PokemonMove.loadMove(m));
continue;
}
if (key === "tags") {
// load battler tags
this.tags = value.map((t: BattlerTag) => loadBattlerTag(t));
continue;
}
this[key] = value;
}
}
}
// TODO: Merge this inside `summmonData` but exclude from save if/when a save data serializer is added
export class PokemonTempSummonData {
/**
* The number of turns this pokemon has spent without switching out.
* Only currently used for positioning the battle cursor.
*/
turnCount = 1;
/**
* The number of turns this pokemon has spent in the active position since the start of the wave
* without switching out.
* Reset on switch and new wave, but not stored in `SummonData` to avoid being written to the save file.
* Used to evaluate "first turn only" conditions such as
* {@linkcode MoveId.FAKE_OUT | Fake Out} and {@linkcode MoveId.FIRST_IMPRESSION | First Impression}).
*/
waveTurnCount = 1;
}
/**
* Persistent data for a {@linkcode Pokemon}.
* Resets at the start of a new battle (but not on switch).
*/
export class PokemonBattleData {
/** Counter tracking direct hits this Pokemon has received during this battle; used for {@linkcode MoveId.RAGE_FIST} */
public hitCount = 0;
/** Whether this Pokemon has eaten a berry this battle; used for {@linkcode MoveId.BELCH} */
public hasEatenBerry = false;
/** Array containing all berries eaten and not yet recovered during this current battle; used by {@linkcode AbilityId.HARVEST} */
public berriesEaten: BerryType[] = [];
constructor(source?: PokemonBattleData | Partial<PokemonBattleData>) {
if (!isNullOrUndefined(source)) {
this.hitCount = source.hitCount ?? 0;
this.hasEatenBerry = source.hasEatenBerry ?? false;
this.berriesEaten = source.berriesEaten ?? [];
}
}
}
/**
* Temporary data for a {@linkcode Pokemon}.
* Resets on new wave/battle start (but not on switch).
*/
export class PokemonWaveData {
/** Whether the pokemon has endured due to a {@linkcode BattlerTagType.ENDURE_TOKEN} */
public endured = false;
/**
* A set of all the abilities this {@linkcode Pokemon} has used in this wave.
* Used to track once per battle conditions, as well as (hopefully) by the updated AI for move effectiveness.
*/
public abilitiesApplied: Set<AbilityId> = new Set<AbilityId>();
/** Whether the pokemon's ability has been revealed or not */
public abilityRevealed = false;
}
/**
* Temporary data for a {@linkcode Pokemon}.
* Resets at the start of a new turn, as well as on switch.
*/
export class PokemonTurnData {
public acted = false;
/** How many times the current move should hit the target(s) */
public hitCount = 0;
/**
* - `-1` = Calculate how many hits are left
* - `0` = Move is finished
*/
public hitsLeft = -1;
public totalDamageDealt = 0;
public singleHitDamageDealt = 0;
public damageTaken = 0;
public attacksReceived: AttackMoveResult[] = [];
public order: number;
public statStagesIncreased = false;
public statStagesDecreased = false;
public moveEffectiveness: TypeDamageMultiplier | null = null;
public combiningPledge?: MoveId;
public switchedInThisTurn = false;
public failedRunAway = false;
public joinedRound = false;
/**
* The amount of times this Pokemon has acted again and used a move in the current turn.
* Used to make sure multi-hits occur properly when the user is
* forced to act again in the same turn, and **must be incremented** by any effects that grant extra actions.
*/
public extraTurns = 0;
/**
* All berries eaten by this pokemon in this turn.
* Saved into {@linkcode PokemonSummonData | SummonData} by {@linkcode AbilityId.CUD_CHEW} on turn end.
* @see {@linkcode PokemonSummonData.berriesEatenLast}
*/
public berriesEaten: BerryType[] = [];
}

View File

@ -3,6 +3,7 @@ import type Move from "./moves/move";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import i18next from "i18next"; import i18next from "i18next";
import { getPokemonNameWithAffix } from "#app/messages";
export enum TerrainType { export enum TerrainType {
NONE, NONE,
@ -96,3 +97,76 @@ export function getTerrainColor(terrainType: TerrainType): [number, number, numb
return [0, 0, 0]; return [0, 0, 0];
} }
/**
* Return the message associated with a terrain effect starting.
* @param terrainType - The {@linkcode TerrainType} starting.
* @returns A string containing the appropriate terrain start text.
*/
export function getTerrainStartMessage(terrainType: TerrainType): string {
switch (terrainType) {
case TerrainType.MISTY:
return i18next.t("terrain:mistyStartMessage");
case TerrainType.ELECTRIC:
return i18next.t("terrain:electricStartMessage");
case TerrainType.GRASSY:
return i18next.t("terrain:grassyStartMessage");
case TerrainType.PSYCHIC:
return i18next.t("terrain:psychicStartMessage");
case TerrainType.NONE:
default:
terrainType satisfies TerrainType.NONE;
console.warn(`${terrainType} unexpectedly provided as terrain type to getTerrainStartMessage!`);
return "";
}
}
/**
* Return the message associated with a terrain effect ceasing to exist.
* @param terrainType - The {@linkcode TerrainType} being cleared.
* @returns A string containing the appropriate terrain clear text.
*/
export function getTerrainClearMessage(terrainType: TerrainType): string {
switch (terrainType) {
case TerrainType.MISTY:
return i18next.t("terrain:mistyClearMessage");
case TerrainType.ELECTRIC:
return i18next.t("terrain:electricClearMessage");
case TerrainType.GRASSY:
return i18next.t("terrain:grassyClearMessage");
case TerrainType.PSYCHIC:
return i18next.t("terrain:psychicClearMessage");
case TerrainType.NONE:
default:
terrainType satisfies TerrainType.NONE;
console.warn(`${terrainType} unexpectedly provided as terrain type to getTerrainClearMessage!`);
return "";
}
}
/**
* Return the message associated with a terrain-induced move/effect blockage.
* @param pokemon - The {@linkcode Pokemon} being protected.
* @param terrainType - The {@linkcode TerrainType} in question
* @returns A string containing the appropriate terrain block text.
*/
export function getTerrainBlockMessage(pokemon: Pokemon, terrainType: TerrainType): string {
switch (terrainType) {
case TerrainType.MISTY:
return i18next.t("terrain:mistyBlockMessage", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
});
case TerrainType.ELECTRIC:
case TerrainType.GRASSY:
case TerrainType.PSYCHIC:
return i18next.t("terrain:defaultBlockMessage", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
terrainName: getTerrainName(terrainType),
});
case TerrainType.NONE:
default:
terrainType satisfies TerrainType.NONE;
console.warn(`${terrainType} unexpectedly provided as terrain type to getTerrainBlockMessage!`);
return "";
}
}

View File

@ -5,7 +5,6 @@ import type Pokemon from "../field/pokemon";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import type Move from "./moves/move"; import type Move from "./moves/move";
import { randSeedInt } from "#app/utils/common"; import { randSeedInt } from "#app/utils/common";
import { TerrainType, getTerrainName } from "./terrain";
import i18next from "i18next"; import i18next from "i18next";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type { Arena } from "#app/field/arena"; import type { Arena } from "#app/field/arena";
@ -235,50 +234,6 @@ export function getWeatherBlockMessage(weatherType: WeatherType): string {
return i18next.t("weather:defaultEffectMessage"); return i18next.t("weather:defaultEffectMessage");
} }
export function getTerrainStartMessage(terrainType: TerrainType): string | null {
switch (terrainType) {
case TerrainType.MISTY:
return i18next.t("terrain:mistyStartMessage");
case TerrainType.ELECTRIC:
return i18next.t("terrain:electricStartMessage");
case TerrainType.GRASSY:
return i18next.t("terrain:grassyStartMessage");
case TerrainType.PSYCHIC:
return i18next.t("terrain:psychicStartMessage");
default:
console.warn("getTerrainStartMessage not defined. Using default null");
return null;
}
}
export function getTerrainClearMessage(terrainType: TerrainType): string | null {
switch (terrainType) {
case TerrainType.MISTY:
return i18next.t("terrain:mistyClearMessage");
case TerrainType.ELECTRIC:
return i18next.t("terrain:electricClearMessage");
case TerrainType.GRASSY:
return i18next.t("terrain:grassyClearMessage");
case TerrainType.PSYCHIC:
return i18next.t("terrain:psychicClearMessage");
default:
console.warn("getTerrainClearMessage not defined. Using default null");
return null;
}
}
export function getTerrainBlockMessage(pokemon: Pokemon, terrainType: TerrainType): string {
if (terrainType === TerrainType.MISTY) {
return i18next.t("terrain:mistyBlockMessage", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
});
}
return i18next.t("terrain:defaultBlockMessage", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
terrainName: getTerrainName(terrainType),
});
}
export interface WeatherPoolEntry { export interface WeatherPoolEntry {
weatherType: WeatherType; weatherType: WeatherType;
weight: number; weight: number;

View File

@ -5,8 +5,6 @@ import { randSeedInt, NumberHolder, isNullOrUndefined, type Constructor } from "
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { import {
getTerrainClearMessage,
getTerrainStartMessage,
getWeatherClearMessage, getWeatherClearMessage,
getWeatherStartMessage, getWeatherStartMessage,
getLegendaryWeatherContinuesMessage, getLegendaryWeatherContinuesMessage,
@ -19,7 +17,7 @@ import type { ArenaTag } from "#app/data/arena-tag";
import { ArenaTrapTag, getArenaTag } from "#app/data/arena-tag"; import { ArenaTrapTag, getArenaTag } from "#app/data/arena-tag";
import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagSide } from "#enums/arena-tag-side";
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import { Terrain, TerrainType } from "#app/data/terrain"; import { Terrain, TerrainType, getTerrainClearMessage, getTerrainStartMessage } from "#app/data/terrain";
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
@ -445,9 +443,9 @@ export class Arena {
CommonAnim.MISTY_TERRAIN + (terrain - 1), CommonAnim.MISTY_TERRAIN + (terrain - 1),
); );
} }
globalScene.phaseManager.queueMessage(getTerrainStartMessage(terrain)!); // TODO: is this bang correct? globalScene.phaseManager.queueMessage(getTerrainStartMessage(terrain));
} else { } else {
globalScene.phaseManager.queueMessage(getTerrainClearMessage(oldTerrainType)!); // TODO: is this bang correct? globalScene.phaseManager.queueMessage(getTerrainClearMessage(oldTerrainType));
} }
globalScene globalScene

View File

@ -1,5 +1,5 @@
import { TextStyle, addTextObject } from "../ui/text"; import { TextStyle, addTextObject } from "../ui/text";
import type { DamageResult } from "./pokemon"; import type { DamageResult } from "../@types/damage-result";
import type Pokemon from "./pokemon"; import type Pokemon from "./pokemon";
import { HitResult } from "#enums/hit-result"; import { HitResult } from "#enums/hit-result";
import { formatStat, fixedInt } from "#app/utils/common"; import { formatStat, fixedInt } from "#app/utils/common";

View File

@ -100,7 +100,6 @@ import {
TarShotTag, TarShotTag,
AutotomizedTag, AutotomizedTag,
PowerTrickTag, PowerTrickTag,
loadBattlerTag,
type GrudgeTag, type GrudgeTag,
} from "../data/battler-tags"; } from "../data/battler-tags";
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
@ -151,7 +150,14 @@ import { getPokemonNameWithAffix } from "#app/messages";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { PokemonAnimType } from "#enums/pokemon-anim-type";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import {
CustomPokemonData,
PokemonBattleData,
PokemonSummonData,
PokemonTempSummonData,
PokemonTurnData,
PokemonWaveData,
} from "#app/data/pokemon/pokemon-data";
import { SwitchType } from "#enums/switch-type"; import { SwitchType } from "#enums/switch-type";
import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesFormKey } from "#enums/species-form-key";
import { getStatusEffectOverlapText } from "#app/data/status-effect"; import { getStatusEffectOverlapText } from "#app/data/status-effect";
@ -169,12 +175,15 @@ import { timedEventManager } from "#app/global-event-manager";
import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader"; import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader";
import { isVirtual, isIgnorePP, MoveUseMode } from "#enums/move-use-mode"; import { isVirtual, isIgnorePP, MoveUseMode } from "#enums/move-use-mode";
import { FieldPosition } from "#enums/field-position"; import { FieldPosition } from "#enums/field-position";
import { LearnMoveSituation } from "#enums/learn-move-situation";
import { HitResult } from "#enums/hit-result"; import { HitResult } from "#enums/hit-result";
import { AiType } from "#enums/ai-type"; import { AiType } from "#enums/ai-type";
import type { MoveResult } from "#enums/move-result";
import { PokemonMove } from "#app/data/moves/pokemon-move"; import { PokemonMove } from "#app/data/moves/pokemon-move";
import type { IllusionData } from "#app/@types/illusion-data";
import type { TurnMove } from "#app/@types/turn-move";
import type { DamageCalculationResult, DamageResult } from "#app/@types/damage-result";
import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#app/@types/ability-types"; import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#app/@types/ability-types";
import { getTerrainBlockMessage } from "#app/data/terrain";
import { LearnMoveSituation } from "#enums/learn-move-situation";
/** Base typeclass for damage parameter methods, used for DRY */ /** Base typeclass for damage parameter methods, used for DRY */
type damageParams = { type damageParams = {
@ -4654,16 +4663,37 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
); );
} }
queueImmuneMessage(quiet: boolean, effect?: StatusEffect): void { /**
if (!effect || quiet) { * Display an immunity message for a failed status application.
* @param quiet - Whether to suppress message and return early
* @param reason - The reason for the status application failure -
* can be "overlap" (already has same status), "other" (generic fail message)
* or a {@linkcode TerrainType} for terrain-based blockages.
* Defaults to "other".
*/
queueStatusImmuneMessage(
quiet: boolean,
reason: "overlap" | "other" | Exclude<TerrainType, TerrainType.NONE> = "other",
): void {
if (quiet) {
return; return;
} }
const message =
effect && this.status?.effect === effect let message: string;
? getStatusEffectOverlapText(effect ?? StatusEffect.NONE, getPokemonNameWithAffix(this)) if (reason === "overlap") {
: i18next.t("abilityTriggers:moveImmunity", { // "XYZ is already XXX!"
message = getStatusEffectOverlapText(this.status?.effect ?? StatusEffect.NONE, getPokemonNameWithAffix(this));
} else if (typeof reason === "number") {
// "XYZ was protected by the XXX terrain!" /
// "XYZ surrounds itself with a protective mist!"
message = getTerrainBlockMessage(this, reason);
} else {
// "It doesn't affect XXX!"
message = i18next.t("abilityTriggers:moveImmunity", {
pokemonNameWithAffix: getPokemonNameWithAffix(this), pokemonNameWithAffix: getPokemonNameWithAffix(this),
}); });
}
globalScene.phaseManager.queueMessage(message); globalScene.phaseManager.queueMessage(message);
} }
@ -4685,11 +4715,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
): boolean { ): boolean {
if (effect !== StatusEffect.FAINT) { if (effect !== StatusEffect.FAINT) {
if (overrideStatus ? this.status?.effect === effect : this.status) { if (overrideStatus ? this.status?.effect === effect : this.status) {
this.queueImmuneMessage(quiet, effect); this.queueStatusImmuneMessage(quiet, overrideStatus ? "overlap" : "other"); // having different status displays generic fail message
return false; return false;
} }
if (this.isGrounded() && !ignoreField && globalScene.arena.terrain?.terrainType === TerrainType.MISTY) { if (this.isGrounded() && !ignoreField && globalScene.arena.terrain?.terrainType === TerrainType.MISTY) {
this.queueImmuneMessage(quiet, effect); this.queueStatusImmuneMessage(quiet, TerrainType.MISTY);
return false; return false;
} }
} }
@ -4726,7 +4756,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (this.isOfType(PokemonType.POISON) || this.isOfType(PokemonType.STEEL)) { if (this.isOfType(PokemonType.POISON) || this.isOfType(PokemonType.STEEL)) {
if (poisonImmunity.includes(true)) { if (poisonImmunity.includes(true)) {
this.queueImmuneMessage(quiet, effect); this.queueStatusImmuneMessage(quiet);
return false; return false;
} }
} }
@ -4734,13 +4764,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
case StatusEffect.PARALYSIS: case StatusEffect.PARALYSIS:
if (this.isOfType(PokemonType.ELECTRIC)) { if (this.isOfType(PokemonType.ELECTRIC)) {
this.queueImmuneMessage(quiet, effect); this.queueStatusImmuneMessage(quiet);
return false; return false;
} }
break; break;
case StatusEffect.SLEEP: case StatusEffect.SLEEP:
if (this.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC) { if (this.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC) {
this.queueImmuneMessage(quiet, effect); this.queueStatusImmuneMessage(quiet, TerrainType.ELECTRIC);
return false; return false;
} }
break; break;
@ -4751,13 +4781,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
globalScene?.arena?.weather?.weatherType && globalScene?.arena?.weather?.weatherType &&
[WeatherType.SUNNY, WeatherType.HARSH_SUN].includes(globalScene.arena.weather.weatherType)) [WeatherType.SUNNY, WeatherType.HARSH_SUN].includes(globalScene.arena.weather.weatherType))
) { ) {
this.queueImmuneMessage(quiet, effect); this.queueStatusImmuneMessage(quiet);
return false; return false;
} }
break; break;
case StatusEffect.BURN: case StatusEffect.BURN:
if (this.isOfType(PokemonType.FIRE)) { if (this.isOfType(PokemonType.FIRE)) {
this.queueImmuneMessage(quiet, effect); this.queueStatusImmuneMessage(quiet);
return false; return false;
} }
break; break;
@ -6790,241 +6820,3 @@ export class EnemyPokemon extends Pokemon {
this.battleInfo.toggleFlyout(visible); this.battleInfo.toggleFlyout(visible);
} }
} }
/**
* Illusion property
*/
interface IllusionData {
basePokemon: {
/** The actual name of the Pokemon */
name: string;
/** The actual nickname of the Pokemon */
nickname: string;
/** Whether the base pokemon is shiny or not */
shiny: boolean;
/** The shiny variant of the base pokemon */
variant: Variant;
/** Whether the fusion species of the base pokemon is shiny or not */
fusionShiny: boolean;
/** The variant of the fusion species of the base pokemon */
fusionVariant: Variant;
};
/** The species of the illusion */
species: SpeciesId;
/** The formIndex of the illusion */
formIndex: number;
/** The gender of the illusion */
gender: Gender;
/** The pokeball of the illusion */
pokeball: PokeballType;
/** The fusion species of the illusion if it's a fusion */
fusionSpecies?: PokemonSpecies;
/** The fusionFormIndex of the illusion */
fusionFormIndex?: number;
/** The fusionGender of the illusion if it's a fusion */
fusionGender?: Gender;
/** The level of the illusion (not used currently) */
level?: number;
}
export interface TurnMove {
move: MoveId;
targets: BattlerIndex[];
useMode: MoveUseMode;
result?: MoveResult;
turn?: number;
}
export interface AttackMoveResult {
move: MoveId;
result: DamageResult;
damage: number;
critical: boolean;
sourceId: number;
sourceBattlerIndex: BattlerIndex;
}
/**
* Persistent in-battle data for a {@linkcode Pokemon}.
* Resets on switch or new battle.
*/
export class PokemonSummonData {
/** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */
public statStages: number[] = [0, 0, 0, 0, 0, 0, 0];
/**
* A queue of moves yet to be executed, used by charging, recharging and frenzy moves.
* So long as this array is nonempty, this Pokemon's corresponding `CommandPhase` will be skipped over entirely
* in favor of using the queued move.
* TODO: Clean up a lot of the code surrounding the move queue.
*/
public moveQueue: TurnMove[] = [];
public tags: BattlerTag[] = [];
public abilitySuppressed = false;
// Overrides for transform.
// TODO: Move these into a separate class & add rage fist hit count
public speciesForm: PokemonSpeciesForm | null = null;
public fusionSpeciesForm: PokemonSpeciesForm | null = null;
public ability: AbilityId | undefined;
public passiveAbility: AbilityId | undefined;
public gender: Gender | undefined;
public fusionGender: Gender | undefined;
public stats: number[] = [0, 0, 0, 0, 0, 0];
public moveset: PokemonMove[] | null;
// If not initialized this value will not be populated from save data.
public types: PokemonType[] = [];
public addedType: PokemonType | null = null;
/** Data pertaining to this pokemon's illusion. */
public illusion: IllusionData | null = null;
public illusionBroken = false;
/** Array containing all berries eaten in the last turn; used by {@linkcode AbilityId.CUD_CHEW} */
public berriesEatenLast: BerryType[] = [];
/**
* An array of all moves this pokemon has used since entering the battle.
* Used for most moves and abilities that check prior move usage or copy already-used moves.
*/
public moveHistory: TurnMove[] = [];
constructor(source?: PokemonSummonData | Partial<PokemonSummonData>) {
if (isNullOrUndefined(source)) {
return;
}
// TODO: Rework this into an actual generic function for use elsewhere
for (const [key, value] of Object.entries(source)) {
if (isNullOrUndefined(value) && this.hasOwnProperty(key)) {
continue;
}
if (key === "moveset") {
this.moveset = value?.map((m: any) => PokemonMove.loadMove(m));
continue;
}
if (key === "tags") {
// load battler tags
this.tags = value.map((t: BattlerTag) => loadBattlerTag(t));
continue;
}
this[key] = value;
}
}
}
// TODO: Merge this inside `summmonData` but exclude from save if/when a save data serializer is added
export class PokemonTempSummonData {
/**
* The number of turns this pokemon has spent without switching out.
* Only currently used for positioning the battle cursor.
*/
turnCount = 1;
/**
* The number of turns this pokemon has spent in the active position since the start of the wave
* without switching out.
* Reset on switch and new wave, but not stored in `SummonData` to avoid being written to the save file.
* Used to evaluate "first turn only" conditions such as
* {@linkcode MoveId.FAKE_OUT | Fake Out} and {@linkcode MoveId.FIRST_IMPRESSION | First Impression}).
*/
waveTurnCount = 1;
}
/**
* Persistent data for a {@linkcode Pokemon}.
* Resets at the start of a new battle (but not on switch).
*/
export class PokemonBattleData {
/** Counter tracking direct hits this Pokemon has received during this battle; used for {@linkcode MoveId.RAGE_FIST} */
public hitCount = 0;
/** Whether this Pokemon has eaten a berry this battle; used for {@linkcode MoveId.BELCH} */
public hasEatenBerry = false;
/** Array containing all berries eaten and not yet recovered during this current battle; used by {@linkcode AbilityId.HARVEST} */
public berriesEaten: BerryType[] = [];
constructor(source?: PokemonBattleData | Partial<PokemonBattleData>) {
if (!isNullOrUndefined(source)) {
this.hitCount = source.hitCount ?? 0;
this.hasEatenBerry = source.hasEatenBerry ?? false;
this.berriesEaten = source.berriesEaten ?? [];
}
}
}
/**
* Temporary data for a {@linkcode Pokemon}.
* Resets on new wave/battle start (but not on switch).
*/
export class PokemonWaveData {
/** Whether the pokemon has endured due to a {@linkcode BattlerTagType.ENDURE_TOKEN} */
public endured = false;
/**
* A set of all the abilities this {@linkcode Pokemon} has used in this wave.
* Used to track once per battle conditions, as well as (hopefully) by the updated AI for move effectiveness.
*/
public abilitiesApplied: Set<AbilityId> = new Set<AbilityId>();
/** Whether the pokemon's ability has been revealed or not */
public abilityRevealed = false;
}
/**
* Temporary data for a {@linkcode Pokemon}.
* Resets at the start of a new turn, as well as on switch.
*/
export class PokemonTurnData {
public acted = false;
/** How many times the current move should hit the target(s) */
public hitCount = 0;
/**
* - `-1` = Calculate how many hits are left
* - `0` = Move is finished
*/
public hitsLeft = -1;
public totalDamageDealt = 0;
public singleHitDamageDealt = 0;
public damageTaken = 0;
public attacksReceived: AttackMoveResult[] = [];
public order: number;
public statStagesIncreased = false;
public statStagesDecreased = false;
public moveEffectiveness: TypeDamageMultiplier | null = null;
public combiningPledge?: MoveId;
public switchedInThisTurn = false;
public failedRunAway = false;
public joinedRound = false;
/**
* The amount of times this Pokemon has acted again and used a move in the current turn.
* Used to make sure multi-hits occur properly when the user is
* forced to act again in the same turn, and **must be incremented** by any effects that grant extra actions.
*/
public extraTurns = 0;
/**
* All berries eaten by this pokemon in this turn.
* Saved into {@linkcode PokemonSummonData | SummonData} by {@linkcode AbilityId.CUD_CHEW} on turn end.
* @see {@linkcode PokemonSummonData.berriesEatenLast}
*/
public berriesEaten: BerryType[] = [];
}
export type DamageResult =
| HitResult.EFFECTIVE
| HitResult.SUPER_EFFECTIVE
| HitResult.NOT_VERY_EFFECTIVE
| HitResult.ONE_HIT_KO
| HitResult.CONFUSION
| HitResult.INDIRECT_KO
| HitResult.INDIRECT;
/** Interface containing the results of a damage calculation for a given move */
export interface DamageCalculationResult {
/** `true` if the move was cancelled (thus suppressing "No Effect" messages) */
cancelled: boolean;
/** The effectiveness of the move */
result: HitResult;
/** The damage dealt by the move */
damage: number;
}

View File

@ -1,18 +1,18 @@
import { type PokeballCounts } from "#app/battle-scene"; import { type PokeballCounts } from "#app/battle-scene";
import { EvolutionItem } from "#app/data/balance/pokemon-evolutions"; import { EvolutionItem } from "#app/data/balance/pokemon-evolutions";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import { FormChangeItem } from "#enums/form-change-item";
import { type ModifierOverride } from "#app/modifier/modifier-type"; import { type ModifierOverride } from "#app/modifier/modifier-type";
import { Variant } from "#app/sprites/variant"; import { Variant } from "#app/sprites/variant";
import { Unlockables } from "#enums/unlockables";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { EggTier } from "#enums/egg-type"; import { EggTier } from "#enums/egg-type";
import { FormChangeItem } from "#enums/form-change-item";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Nature } from "#enums/nature";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
@ -20,6 +20,7 @@ import { Stat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { TimeOfDay } from "#enums/time-of-day"; import { TimeOfDay } from "#enums/time-of-day";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { Unlockables } from "#enums/unlockables";
import { VariantTier } from "#enums/variant-tier"; import { VariantTier } from "#enums/variant-tier";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
@ -159,10 +160,20 @@ class DefaultOverrides {
readonly MOVESET_OVERRIDE: MoveId | Array<MoveId> = []; readonly MOVESET_OVERRIDE: MoveId | Array<MoveId> = [];
readonly SHINY_OVERRIDE: boolean | null = null; readonly SHINY_OVERRIDE: boolean | null = null;
readonly VARIANT_OVERRIDE: Variant | null = null; readonly VARIANT_OVERRIDE: Variant | null = null;
/**
* Overrides the IVs of player pokemon. Values must never be outside the range `0` to `31`!
* - If set to a number between `0` and `31`, set all IVs of all player pokemon to that number.
* - If set to an array, set the IVs of all player pokemon to that array. Array length must be exactly `6`!
* - If set to `null`, disable the override.
*/
readonly IVS_OVERRIDE: number | number[] | null = null;
/** Override the nature of all player pokemon to the specified nature. Disabled if `null`. */
readonly NATURE_OVERRIDE: Nature | null = null;
// -------------------------- // --------------------------
// OPPONENT / ENEMY OVERRIDES // OPPONENT / ENEMY OVERRIDES
// -------------------------- // --------------------------
// TODO: rename `OPP_` to `ENEMY_`
readonly OPP_SPECIES_OVERRIDE: SpeciesId | number = 0; readonly OPP_SPECIES_OVERRIDE: SpeciesId | number = 0;
/** /**
* This will make all opponents fused Pokemon * This will make all opponents fused Pokemon
@ -181,7 +192,15 @@ class DefaultOverrides {
readonly OPP_MOVESET_OVERRIDE: MoveId | Array<MoveId> = []; readonly OPP_MOVESET_OVERRIDE: MoveId | Array<MoveId> = [];
readonly OPP_SHINY_OVERRIDE: boolean | null = null; readonly OPP_SHINY_OVERRIDE: boolean | null = null;
readonly OPP_VARIANT_OVERRIDE: Variant | null = null; readonly OPP_VARIANT_OVERRIDE: Variant | null = null;
readonly OPP_IVS_OVERRIDE: number | number[] = []; /**
* Overrides the IVs of enemy pokemon. Values must never be outside the range `0` to `31`!
* - If set to a number between `0` and `31`, set all IVs of all enemy pokemon to that number.
* - If set to an array, set the IVs of all enemy pokemon to that array. Array length must be exactly `6`!
* - If set to `null`, disable the override.
*/
readonly ENEMY_IVS_OVERRIDE: number | number[] | null = null;
/** Override the nature of all enemy pokemon to the specified nature. Disabled if `null`. */
readonly ENEMY_NATURE_OVERRIDE: Nature | null = null;
readonly OPP_FORM_OVERRIDES: Partial<Record<SpeciesId, number>> = {}; readonly OPP_FORM_OVERRIDES: Partial<Record<SpeciesId, number>> = {};
/** /**
* Override to give the enemy Pokemon a given amount of health segments * Override to give the enemy Pokemon a given amount of health segments

View File

@ -11,7 +11,8 @@ import { BattlerTagType } from "#app/enums/battler-tag-type";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import type { PlayerPokemon, TurnMove } from "#app/field/pokemon"; import type { PlayerPokemon } from "#app/field/pokemon";
import type { TurnMove } from "#app/@types/turn-move";
import { FieldPosition } from "#enums/field-position"; import { FieldPosition } from "#enums/field-position";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { Command } from "#enums/command"; import { Command } from "#enums/command";

View File

@ -1,7 +1,7 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import { BattleSpec } from "#enums/battle-spec"; import { BattleSpec } from "#enums/battle-spec";
import type { DamageResult } from "#app/field/pokemon"; import type { DamageResult } from "#app/@types/damage-result";
import { HitResult } from "#enums/hit-result"; import { HitResult } from "#enums/hit-result";
import { fixedInt } from "#app/utils/common"; import { fixedInt } from "#app/utils/common";
import { PokemonPhase } from "#app/phases/pokemon-phase"; import { PokemonPhase } from "#app/phases/pokemon-phase";

View File

@ -21,7 +21,8 @@ import { MoveTarget } from "#enums/MoveTarget";
import { MoveCategory } from "#enums/MoveCategory"; import { MoveCategory } from "#enums/MoveCategory";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms/form-change-triggers";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import type { DamageResult, TurnMove } from "#app/field/pokemon"; import type { DamageResult } from "#app/@types/damage-result";
import type { TurnMove } from "#app/@types/turn-move";
import { PokemonMove } from "#app/data/moves/pokemon-move"; import { PokemonMove } from "#app/data/moves/pokemon-move";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { MoveResult } from "#enums/move-result"; import { MoveResult } from "#enums/move-result";

View File

@ -11,7 +11,7 @@ import { MoveFlags } from "#enums/MoveFlags";
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms/form-change-triggers";
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect"; import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { getTerrainBlockMessage, getWeatherBlockMessage } from "#app/data/weather"; import { getWeatherBlockMessage } from "#app/data/weather";
import { MoveUsedEvent } from "#app/events/battle-scene"; import { MoveUsedEvent } from "#app/events/battle-scene";
import type { PokemonMove } from "#app/data/moves/pokemon-move"; import type { PokemonMove } from "#app/data/moves/pokemon-move";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
@ -26,6 +26,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next"; import i18next from "i18next";
import { getTerrainBlockMessage } from "#app/data/terrain";
import { isVirtual, isIgnorePP, isReflected, MoveUseMode, isIgnoreStatus } from "#enums/move-use-mode"; import { isVirtual, isIgnorePP, isReflected, MoveUseMode, isIgnoreStatus } from "#enums/move-use-mode";
import { frenzyMissFunc } from "#app/data/moves/move-utils"; import { frenzyMissFunc } from "#app/data/moves/move-utils";

View File

@ -6,14 +6,14 @@ import { PokeballType } from "#enums/pokeball";
import { getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { getPokemonSpeciesForm } from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { Status } from "../data/status-effect"; import { Status } from "../data/status-effect";
import Pokemon, { EnemyPokemon, PokemonBattleData, PokemonSummonData } from "../field/pokemon"; import Pokemon, { EnemyPokemon } from "../field/pokemon";
import { PokemonMove } from "#app/data/moves/pokemon-move"; import { PokemonMove } from "#app/data/moves/pokemon-move";
import { TrainerSlot } from "#enums/trainer-slot"; import { TrainerSlot } from "#enums/trainer-slot";
import type { Variant } from "#app/sprites/variant"; import type { Variant } from "#app/sprites/variant";
import type { BiomeId } from "#enums/biome-id"; import type { BiomeId } from "#enums/biome-id";
import type { MoveId } from "#enums/move-id"; import type { MoveId } from "#enums/move-id";
import type { SpeciesId } from "#enums/species-id"; import type { SpeciesId } from "#enums/species-id";
import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CustomPokemonData, PokemonBattleData, PokemonSummonData } from "#app/data/pokemon/pokemon-data";
import type { PokemonType } from "#enums/pokemon-type"; import type { PokemonType } from "#enums/pokemon-type";
export default class PokemonData { export default class PokemonData {

View File

@ -4,7 +4,7 @@ import { defaultStarterSpecies } from "#app/constants";
import { AbilityAttr } from "#enums/ability-attr"; import { AbilityAttr } from "#enums/ability-attr";
import { DexAttr } from "#enums/dex-attr"; import { DexAttr } from "#enums/dex-attr";
import { allSpecies } from "#app/data/data-lists"; import { allSpecies } from "#app/data/data-lists";
import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CustomPokemonData } from "#app/data/pokemon/pokemon-data";
import { isNullOrUndefined } from "#app/utils/common"; import { isNullOrUndefined } from "#app/utils/common";
import type { SystemSaveMigrator } from "#app/@types/SystemSaveMigrator"; import type { SystemSaveMigrator } from "#app/@types/SystemSaveMigrator";
import type { SettingsSaveMigrator } from "#app/@types/SettingsSaveMigrator"; import type { SettingsSaveMigrator } from "#app/@types/SettingsSaveMigrator";

View File

@ -1,6 +1,6 @@
import type { SessionSaveMigrator } from "#app/@types/SessionSaveMigrator"; import type { SessionSaveMigrator } from "#app/@types/SessionSaveMigrator";
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import type { TurnMove } from "#app/field/pokemon"; import type { TurnMove } from "#app/@types/turn-move";
import type { MoveResult } from "#enums/move-result"; import type { MoveResult } from "#enums/move-result";
import type { SessionSaveData } from "#app/system/game-data"; import type { SessionSaveData } from "#app/system/game-data";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";

View File

@ -1,4 +1,5 @@
import type { PlayerPokemon, TurnMove } from "#app/field/pokemon"; import type { PlayerPokemon } from "#app/field/pokemon";
import type { TurnMove } from "#app/@types/turn-move";
import type { PokemonMove } from "#app/data/moves/pokemon-move"; import type { PokemonMove } from "#app/data/moves/pokemon-move";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { MoveResult } from "#enums/move-result"; import { MoveResult } from "#enums/move-result";

View File

@ -28,8 +28,10 @@ describe("Abilities - Gorilla Tactics", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
.battleStyle("single") .battleStyle("single")
.criticalHits(false)
.enemyAbility(AbilityId.BALL_FETCH) .enemyAbility(AbilityId.BALL_FETCH)
.enemySpecies(SpeciesId.MAGIKARP) .enemySpecies(SpeciesId.MAGIKARP)
.enemyMoveset(MoveId.SPLASH)
.enemyLevel(30) .enemyLevel(30)
.moveset([MoveId.SPLASH, MoveId.TACKLE, MoveId.GROWL, MoveId.METRONOME]) .moveset([MoveId.SPLASH, MoveId.TACKLE, MoveId.GROWL, MoveId.METRONOME])
.ability(AbilityId.GORILLA_TACTICS); .ability(AbilityId.GORILLA_TACTICS);
@ -42,7 +44,6 @@ describe("Abilities - Gorilla Tactics", () => {
const initialAtkStat = darmanitan.getStat(Stat.ATK); const initialAtkStat = darmanitan.getStat(Stat.ATK);
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);
await game.move.forceEnemyMove(MoveId.SPLASH);
await game.toEndOfTurn(); await game.toEndOfTurn();
expect(darmanitan.getStat(Stat.ATK, false)).toBeCloseTo(initialAtkStat * 1.5); expect(darmanitan.getStat(Stat.ATK, false)).toBeCloseTo(initialAtkStat * 1.5);
@ -59,7 +60,6 @@ describe("Abilities - Gorilla Tactics", () => {
// First turn, lock move to Growl // First turn, lock move to Growl
game.move.select(MoveId.GROWL); game.move.select(MoveId.GROWL);
await game.move.forceEnemyMove(MoveId.SPLASH);
await game.toNextTurn(); await game.toNextTurn();
// Second turn, Growl is interrupted by Disable // Second turn, Growl is interrupted by Disable
@ -72,7 +72,7 @@ describe("Abilities - Gorilla Tactics", () => {
// Third turn, Struggle is used // Third turn, Struggle is used
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);
await game.move.forceEnemyMove(MoveId.SPLASH); //prevent protect from being used by the enemy await game.move.forceEnemyMove(MoveId.SPLASH); // prevent disable from being used by the enemy
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEndPhase"); await game.phaseInterceptor.to("MoveEndPhase");
@ -106,11 +106,13 @@ describe("Abilities - Gorilla Tactics", () => {
const darmanitan = game.field.getPlayerPokemon(); const darmanitan = game.field.getPlayerPokemon();
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);
await game.move.selectEnemyMove(MoveId.PROTECT); await game.move.forceEnemyMove(MoveId.PROTECT);
await game.toEndOfTurn(); await game.toEndOfTurn();
expect(darmanitan.isMoveRestricted(MoveId.SPLASH)).toBe(true); expect(darmanitan.isMoveRestricted(MoveId.SPLASH)).toBe(true);
expect(darmanitan.isMoveRestricted(MoveId.TACKLE)).toBe(false); expect(darmanitan.isMoveRestricted(MoveId.TACKLE)).toBe(false);
const enemy = game.field.getEnemyPokemon();
expect(enemy.hp).toBe(enemy.getMaxHp());
}); });
it("should activate when a move is succesfully executed but misses", async () => { it("should activate when a move is succesfully executed but misses", async () => {
@ -119,7 +121,6 @@ describe("Abilities - Gorilla Tactics", () => {
const darmanitan = game.field.getPlayerPokemon(); const darmanitan = game.field.getPlayerPokemon();
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);
await game.move.selectEnemyMove(MoveId.SPLASH);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.move.forceMiss(); await game.move.forceMiss();
await game.toEndOfTurn(); await game.toEndOfTurn();

View File

@ -108,7 +108,7 @@ describe("Abilities - Wimp Out", () => {
}); });
it("Trapping moves do not prevent Wimp Out from activating.", async () => { it("Trapping moves do not prevent Wimp Out from activating.", async () => {
game.override.enemyMoveset([MoveId.SPIRIT_SHACKLE]).startingLevel(53).enemyLevel(45); game.override.enemyMoveset([MoveId.SPIRIT_SHACKLE]).startingLevel(1).passiveAbility(AbilityId.STURDY);
await game.classicMode.startBattle([SpeciesId.WIMPOD, SpeciesId.TYRUNT]); await game.classicMode.startBattle([SpeciesId.WIMPOD, SpeciesId.TYRUNT]);
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);
@ -123,7 +123,7 @@ describe("Abilities - Wimp Out", () => {
}); });
it("If this Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out.", async () => { it("If this Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out.", async () => {
game.override.startingLevel(95).enemyMoveset([MoveId.U_TURN]); game.override.startingLevel(1).enemyMoveset([MoveId.U_TURN]).passiveAbility(AbilityId.STURDY);
await game.classicMode.startBattle([SpeciesId.WIMPOD, SpeciesId.TYRUNT]); await game.classicMode.startBattle([SpeciesId.WIMPOD, SpeciesId.TYRUNT]);
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);

View File

@ -1,6 +1,6 @@
import { StockpilingTag } from "#app/data/battler-tags"; import { StockpilingTag } from "#app/data/battler-tags";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { PokemonSummonData } from "#app/field/pokemon"; import { PokemonSummonData } from "#app/data/pokemon/pokemon-data";
import * as messages from "#app/messages"; import * as messages from "#app/messages";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";

View File

@ -1,5 +1,6 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { PokemonTurnData, TurnMove } from "#app/field/pokemon"; import type { TurnMove } from "#app/@types/turn-move";
import type { PokemonTurnData } from "#app/data/pokemon/pokemon-data";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { MoveResult } from "#enums/move-result"; import { MoveResult } from "#enums/move-result";
import type BattleScene from "#app/battle-scene"; import type BattleScene from "#app/battle-scene";

View File

@ -5,7 +5,7 @@ import { PokeballType } from "#enums/pokeball";
import type BattleScene from "#app/battle-scene"; import type BattleScene from "#app/battle-scene";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CustomPokemonData } from "#app/data/pokemon/pokemon-data";
describe("Spec - Pokemon", () => { describe("Spec - Pokemon", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;

View File

@ -42,7 +42,7 @@ describe("Moves - Heal Block", () => {
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
player.damageAndUpdate(enemy.getMaxHp() - 1); player.damageAndUpdate(player.getMaxHp() - 1);
game.move.select(MoveId.ABSORB); game.move.select(MoveId.ABSORB);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);

View File

@ -1,12 +1,13 @@
import { BattlerIndex } from "#enums/battler-index"; import type { TurnMove } from "#app/@types/turn-move";
import { RandomMoveAttr } from "#app/data/moves/move";
import { allMoves } from "#app/data/data-lists"; import { allMoves } from "#app/data/data-lists";
import { RandomMoveAttr } from "#app/data/moves/move";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { MoveResult } from "#enums/move-result";
import type { MovePhase } from "#app/phases/move-phase"; import type { MovePhase } from "#app/phases/move-phase";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { MoveUseMode } from "#enums/move-use-mode"; import { BattlerIndex } from "#enums/battler-index";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MoveResult } from "#enums/move-result";
import { MoveUseMode } from "#enums/move-use-mode";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/gameManager";
@ -202,21 +203,32 @@ describe("Moves - Instruct", () => {
game.override.battleStyle("double").enemyMoveset(MoveId.SPLASH).enemySpecies(SpeciesId.MAGIKARP).enemyLevel(1); game.override.battleStyle("double").enemyMoveset(MoveId.SPLASH).enemySpecies(SpeciesId.MAGIKARP).enemyLevel(1);
await game.classicMode.startBattle([SpeciesId.HISUI_ELECTRODE, SpeciesId.KOMMO_O]); await game.classicMode.startBattle([SpeciesId.HISUI_ELECTRODE, SpeciesId.KOMMO_O]);
const [electrode, kommo_o] = game.scene.getPlayerField()!; const [electrode, kommo_o] = game.scene.getPlayerField();
game.move.changeMoveset(electrode, MoveId.CHLOROBLAST); game.move.changeMoveset(electrode, MoveId.THUNDERBOLT);
game.move.changeMoveset(kommo_o, MoveId.INSTRUCT); game.move.changeMoveset(kommo_o, MoveId.INSTRUCT);
game.move.select(MoveId.CHLOROBLAST, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.select(MoveId.THUNDERBOLT, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
game.move.select(MoveId.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); game.move.select(MoveId.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
await game.phaseInterceptor.to("BerryPhase"); await game.toEndOfTurn();
// Chloroblast always deals 50% max HP% recoil UNLESS you whiff expect(electrode.getMoveHistory()).toEqual(
// due to lack of targets or similar, expect.arrayContaining([
// so all we have to do is check whether electrode fainted or not. expect.objectContaining<TurnMove>({
// Naturally, both karps should also be dead as well. result: MoveResult.SUCCESS,
expect(electrode.isFainted()).toBe(true); move: MoveId.THUNDERBOLT,
const [karp1, karp2] = game.scene.getEnemyField()!; targets: [BattlerIndex.ENEMY],
useMode: MoveUseMode.NORMAL,
}),
expect.objectContaining<TurnMove>({
result: MoveResult.SUCCESS,
move: MoveId.THUNDERBOLT,
targets: [BattlerIndex.ENEMY_2],
useMode: MoveUseMode.NORMAL,
}),
]),
);
const [karp1, karp2] = game.scene.getEnemyField();
expect(karp1.isFainted()).toBe(true); expect(karp1.isFainted()).toBe(true);
expect(karp2.isFainted()).toBe(true); expect(karp2.isFainted()).toBe(true);
}); });

View File

@ -1,7 +1,7 @@
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { allMoves } from "#app/data/data-lists"; import { allMoves } from "#app/data/data-lists";
import { BattlerTagType } from "#app/enums/battler-tag-type"; import { BattlerTagType } from "#app/enums/battler-tag-type";
import type { DamageCalculationResult } from "#app/field/pokemon"; import type { DamageCalculationResult } from "#app/@types/damage-result";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";

View File

@ -24,7 +24,7 @@ import { BerryModifier, PokemonBaseStatTotalModifier } from "#app/modifier/modif
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils"; import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils";
import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CustomPokemonData } from "#app/data/pokemon/pokemon-data";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { MovePhase } from "#app/phases/move-phase"; import { MovePhase } from "#app/phases/move-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase";

View File

@ -1,15 +1,16 @@
import { BattleStyle } from "#app/enums/battle-style"; import { BattleStyle } from "#app/enums/battle-style";
import type { SpeciesId } from "#enums/species-id";
import { getGameMode } from "#app/game-mode"; import { getGameMode } from "#app/game-mode";
import { GameModes } from "#enums/game-modes";
import overrides from "#app/overrides"; import overrides from "#app/overrides";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { EncounterPhase } from "#app/phases/encounter-phase"; import { EncounterPhase } from "#app/phases/encounter-phase";
import { SelectStarterPhase } from "#app/phases/select-starter-phase"; import { SelectStarterPhase } from "#app/phases/select-starter-phase";
import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { GameModes } from "#enums/game-modes";
import { Nature } from "#enums/nature";
import type { SpeciesId } from "#enums/species-id";
import { UiMode } from "#enums/ui-mode"; import { UiMode } from "#enums/ui-mode";
import { generateStarter } from "../gameManagerUtils"; import { generateStarter } from "#test/testUtils/gameManagerUtils";
import { GameManagerHelper } from "./gameManagerHelper"; import { GameManagerHelper } from "#test/testUtils/helpers/gameManagerHelper";
/** /**
* Helper to handle classic-mode specific operations. * Helper to handle classic-mode specific operations.
@ -36,6 +37,12 @@ export class ClassicModeHelper extends GameManagerHelper {
if (this.game.override.disableShinies) { if (this.game.override.disableShinies) {
this.game.override.shiny(false).enemyShiny(false); this.game.override.shiny(false).enemyShiny(false);
} }
if (this.game.override.normalizeIVs) {
this.game.override.playerIVs(31).enemyIVs(31);
}
if (this.game.override.normalizeNatures) {
this.game.override.nature(Nature.HARDY).enemyNature(Nature.HARDY);
}
this.game.onNextPrompt("TitlePhase", UiMode.TITLE, () => { this.game.onNextPrompt("TitlePhase", UiMode.TITLE, () => {
this.game.scene.gameMode = getGameMode(GameModes.CLASSIC); this.game.scene.gameMode = getGameMode(GameModes.CLASSIC);

View File

@ -1,35 +1,55 @@
import type { Variant } from "#app/sprites/variant"; /** biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
import type { NewArenaEvent } from "#app/events/battle-scene";
/** biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
import { Weather } from "#app/data/weather"; import { Weather } from "#app/data/weather";
import { AbilityId } from "#enums/ability-id";
import type { ModifierOverride } from "#app/modifier/modifier-type"; import type { ModifierOverride } from "#app/modifier/modifier-type";
import type { BattleStyle } from "#app/overrides"; import type { BattleStyle, RandomTrainerOverride } from "#app/overrides";
import Overrides, { defaultOverrides } from "#app/overrides"; import Overrides, { defaultOverrides } from "#app/overrides";
import type { Unlockables } from "#enums/unlockables"; import type { Variant } from "#app/sprites/variant";
import { coerceArray, shiftCharCodes } from "#app/utils/common";
import { AbilityId } from "#enums/ability-id";
import type { BattleType } from "#enums/battle-type";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import type { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import type { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Nature } from "#enums/nature";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import type { Unlockables } from "#enums/unlockables";
import type { WeatherType } from "#enums/weather-type"; import type { WeatherType } from "#enums/weather-type";
import { GameManagerHelper } from "#test/testUtils/helpers/gameManagerHelper";
import { expect, vi } from "vitest"; import { expect, vi } from "vitest";
import { GameManagerHelper } from "./gameManagerHelper";
import { coerceArray, shiftCharCodes } from "#app/utils/common";
import type { RandomTrainerOverride } from "#app/overrides";
import type { BattleType } from "#enums/battle-type";
/** /**
* Helper to handle overrides in tests * Helper to handle overrides in tests
*/ */
export class OverridesHelper extends GameManagerHelper { export class OverridesHelper extends GameManagerHelper {
/** If `true`, removes the starting items from enemies at the start of each test; default `true` */ /**
* If `true`, removes the starting items from enemies at the start of each test.
* @defaultValue `true`
*/
public removeEnemyStartingItems = true; public removeEnemyStartingItems = true;
/** If `true`, sets the shiny overrides to disable shinies at the start of each test; default `true` */ /**
* If `true`, sets the shiny overrides to disable shinies at the start of each test.
* @defaultValue `true`
*/
public disableShinies = true; public disableShinies = true;
/**
* If `true`, will set the IV overrides for player and enemy pokemon to `31` at the start of each test.
* @defaultValue `true`
*/
public normalizeIVs = true;
/**
* If `true`, will set the Nature overrides for player and enemy pokemon to a neutral nature at the start of each test.
* @defaultValue `true`
*/
public normalizeNatures = true;
/** /**
* Override the starting biome * Override the starting biome
* @warning Any event listeners that are attached to [NewArenaEvent](events\battle-scene.ts) may need to be handled down the line * @warning Any event listeners that are attached to {@linkcode NewArenaEvent} may need to be handled down the line
* @param biome - The biome to set * @param biome - The biome to set
*/ */
public startingBiome(biome: BiomeId): this { public startingBiome(biome: BiomeId): this {
@ -219,6 +239,80 @@ export class OverridesHelper extends GameManagerHelper {
return this; return this;
} }
/**
* Overrides the IVs of the player pokemon
* @param ivs - If set to a number, all IVs are set to the same value. Must be between `0` and `31`!
*
* If set to an array, that array is applied to the pokemon's IV field as-is.
* All values must be between `0` and `31`, and the array must be of exactly length `6`!
*
* If set to `null`, the override is disabled.
* @returns `this`
*/
public playerIVs(ivs: number | number[] | null): this {
this.normalizeIVs = false;
vi.spyOn(Overrides, "IVS_OVERRIDE", "get").mockReturnValue(ivs);
if (ivs === null) {
this.log("Player IVs override disabled!");
} else {
this.log(`Player IVs set to ${ivs}!`);
}
return this;
}
/**
* Overrides the nature of the player's pokemon
* @param nature - The nature to set, or `null` to disable the override.
* @returns `this`
*/
public nature(nature: Nature | null): this {
this.normalizeNatures = false;
vi.spyOn(Overrides, "NATURE_OVERRIDE", "get").mockReturnValue(nature);
if (nature === null) {
this.log("Player Nature override disabled!");
} else {
this.log(`Player Nature set to ${Nature[nature]} (=${nature})!`);
}
return this;
}
/**
* Overrides the IVs of the enemy pokemon
* @param ivs - If set to a number, all IVs are set to the same value. Must be between `0` and `31`!
*
* If set to an array, that array is applied to the pokemon's IV field as-is.
* All values must be between `0` and `31`, and the array must be of exactly length `6`!
*
* If set to `null`, the override is disabled.
* @returns `this`
*/
public enemyIVs(ivs: number | number[] | null): this {
this.normalizeIVs = false;
vi.spyOn(Overrides, "ENEMY_IVS_OVERRIDE", "get").mockReturnValue(ivs);
if (ivs === null) {
this.log("Enemy IVs override disabled!");
} else {
this.log(`Enemy IVs set to ${ivs}!`);
}
return this;
}
/**
* Overrides the nature of the enemy's pokemon
* @param nature - The nature to set, or `null` to disable the override.
* @returns `this`
*/
public enemyNature(nature: Nature | null): this {
this.normalizeNatures = false;
vi.spyOn(Overrides, "ENEMY_NATURE_OVERRIDE", "get").mockReturnValue(nature);
if (nature === null) {
this.log("Enemy Nature override disabled!");
} else {
this.log(`Enemy Nature set to ${Nature[nature]} (=${nature})!`);
}
return this;
}
/** /**
* Override each wave to not have standard trainer battles * Override each wave to not have standard trainer battles
* @returns `this` * @returns `this`