Merge branch 'beta' into sky-battle-me

This commit is contained in:
José Marques 2025-07-01 18:41:32 +01:00
commit c15dcd167e
37 changed files with 555 additions and 346 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 absolutely *feel free to reach out 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. 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.
## 📄 Table of Contents ## 📄 Table of Contents

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

@ -7263,11 +7263,14 @@ export function initAbilities() {
new Ability(AbilityId.MERCILESS, 7) new Ability(AbilityId.MERCILESS, 7)
.attr(ConditionalCritAbAttr, (_user, target, _move) => target?.status?.effect === StatusEffect.TOXIC || target?.status?.effect === StatusEffect.POISON), .attr(ConditionalCritAbAttr, (_user, target, _move) => target?.status?.effect === StatusEffect.TOXIC || target?.status?.effect === StatusEffect.POISON),
new Ability(AbilityId.SHIELDS_DOWN, 7, -1) new Ability(AbilityId.SHIELDS_DOWN, 7, -1)
.attr(PostBattleInitFormChangeAbAttr, () => 0) // Change into Meteor Form on switch-in or turn end if HP >= 50%,
// or Core Form if HP <= 50%.
.attr(PostBattleInitFormChangeAbAttr, p => p.formIndex % 7)
.attr(PostSummonFormChangeAbAttr, p => p.formIndex % 7 + (p.getHpRatio() <= 0.5 ? 7 : 0)) .attr(PostSummonFormChangeAbAttr, p => p.formIndex % 7 + (p.getHpRatio() <= 0.5 ? 7 : 0))
.attr(PostTurnFormChangeAbAttr, p => p.formIndex % 7 + (p.getHpRatio() <= 0.5 ? 7 : 0)) .attr(PostTurnFormChangeAbAttr, p => p.formIndex % 7 + (p.getHpRatio() <= 0.5 ? 7 : 0))
.conditionalAttr(p => p.formIndex !== 7, StatusEffectImmunityAbAttr) // All variants of Meteor Form are immune to status effects & Yawn
.conditionalAttr(p => p.formIndex !== 7, BattlerTagImmunityAbAttr, BattlerTagType.DROWSY) .conditionalAttr(p => p.formIndex < 7, StatusEffectImmunityAbAttr)
.conditionalAttr(p => p.formIndex < 7, BattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
.attr(NoFusionAbilityAbAttr) .attr(NoFusionAbilityAbAttr)
.attr(NoTransformAbilityAbAttr) .attr(NoTransformAbilityAbAttr)
.uncopiable() .uncopiable()
@ -7333,12 +7336,11 @@ export function initAbilities() {
.unsuppressable() .unsuppressable()
.bypassFaint(), .bypassFaint(),
new Ability(AbilityId.POWER_CONSTRUCT, 7) new Ability(AbilityId.POWER_CONSTRUCT, 7)
.conditionalAttr(pokemon => pokemon.formIndex === 2 || pokemon.formIndex === 4, PostBattleInitFormChangeAbAttr, () => 2) // Change to 10% complete or 50% complete on switchout/turn end if at <50% HP;
.conditionalAttr(pokemon => pokemon.formIndex === 3 || pokemon.formIndex === 5, PostBattleInitFormChangeAbAttr, () => 3) // revert to 10% PC or 50% PC before a new battle starts
.conditionalAttr(pokemon => pokemon.formIndex === 2 || pokemon.formIndex === 4, PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2) .conditionalAttr(p => p.formIndex === 4 || p.formIndex === 5, PostBattleInitFormChangeAbAttr, p => p.formIndex - 2)
.conditionalAttr(pokemon => pokemon.formIndex === 2 || pokemon.formIndex === 4, PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2) .conditionalAttr(p => p.getHpRatio() <= 0.5 && (p.formIndex === 2 || p.formIndex === 3), PostSummonFormChangeAbAttr, p => p.formIndex + 2)
.conditionalAttr(pokemon => pokemon.formIndex === 3 || pokemon.formIndex === 5, PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "10-complete" ? 5 : 3) .conditionalAttr(p => p.getHpRatio() <= 0.5 && (p.formIndex === 2 || p.formIndex === 3), PostTurnFormChangeAbAttr, p => p.formIndex + 2)
.conditionalAttr(pokemon => pokemon.formIndex === 3 || pokemon.formIndex === 5, PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "10-complete" ? 5 : 3)
.attr(NoFusionAbilityAbAttr) .attr(NoFusionAbilityAbAttr)
.uncopiable() .uncopiable()
.unreplaceable() .unreplaceable()

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

@ -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";
@ -171,8 +177,10 @@ import { isVirtual, isIgnorePP, MoveUseMode } from "#enums/move-use-mode";
import { FieldPosition } from "#enums/field-position"; import { FieldPosition } from "#enums/field-position";
import { HitResult } from "#enums/hit-result"; import { HitResult } from "#enums/hit-result";
import { 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 { getTerrainBlockMessage } from "#app/data/terrain";
import { LearnMoveSituation } from "#enums/learn-move-situation"; import { LearnMoveSituation } from "#enums/learn-move-situation";
@ -6812,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

@ -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

@ -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`