Merge branch 'beta' into future-sight

This commit is contained in:
Bertie690 2025-07-22 18:44:29 -04:00 committed by GitHub
commit 0def2279a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1004 additions and 557 deletions

48
src/@types/arena-tags.ts Normal file
View File

@ -0,0 +1,48 @@
import type { ArenaTagTypeMap } from "#data/arena-tag";
import type { ArenaTagType } from "#enums/arena-tag-type";
import type { NonFunctionProperties } from "./type-helpers";
/** Subset of {@linkcode ArenaTagType}s that apply some negative effect to pokemon that switch in ({@link https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards#List_of_traps | entry hazards} and Imprison. */
export type ArenaTrapTagType =
| ArenaTagType.STICKY_WEB
| ArenaTagType.SPIKES
| ArenaTagType.TOXIC_SPIKES
| ArenaTagType.STEALTH_ROCK
| ArenaTagType.IMPRISON;
/** Subset of {@linkcode ArenaTagType}s that are considered delayed attacks */
export type ArenaDelayedAttackTagType = ArenaTagType.FUTURE_SIGHT | ArenaTagType.DOOM_DESIRE;
/** Subset of {@linkcode ArenaTagType}s that create {@link https://bulbapedia.bulbagarden.net/wiki/Category:Screen-creating_moves | screens}. */
export type ArenaScreenTagType = ArenaTagType.REFLECT | ArenaTagType.LIGHT_SCREEN | ArenaTagType.AURORA_VEIL;
/** Subset of {@linkcode ArenaTagType}s for moves that add protection */
export type TurnProtectArenaTagType =
| ArenaTagType.QUICK_GUARD
| ArenaTagType.WIDE_GUARD
| ArenaTagType.MAT_BLOCK
| ArenaTagType.CRAFTY_SHIELD;
/** Subset of {@linkcode ArenaTagType}s that cannot persist across turns, and thus should not be serialized in {@linkcode SessionSaveData}. */
export type NonSerializableArenaTagType = ArenaTagType.NONE | TurnProtectArenaTagType | ArenaTagType.ION_DELUGE;
/** Subset of {@linkcode ArenaTagType}s that may persist across turns, and thus must be serialized in {@linkcode SessionSaveData}. */
export type SerializableArenaTagType = Exclude<ArenaTagType, NonSerializableArenaTagType>;
/**
* Type-safe representation of the serializable data of an ArenaTag
*/
export type ArenaTagTypeData = NonFunctionProperties<
ArenaTagTypeMap[keyof {
[K in keyof ArenaTagTypeMap as K extends SerializableArenaTagType ? K : never]: ArenaTagTypeMap[K];
}]
>;
/** Dummy, typescript-only declaration to ensure that
* {@linkcode ArenaTagTypeMap} has a map for all ArenaTagTypes.
*
* If an arena tag is missing from the map, typescript will throw an error on this statement.
*
* Does not actually exist at runtime, so it must not be used!
*/
declare const EnsureAllArenaTagTypesAreMapped: ArenaTagTypeMap[ArenaTagType] & never;

View File

@ -44,3 +44,34 @@ export type Mutable<T> = {
export type InferKeys<O extends Record<keyof any, unknown>, V extends EnumValues<O>> = {
[K in keyof O]: O[K] extends V ? K : never;
}[keyof O];
/**
* Type helper that matches any `Function` type. Equivalent to `Function`, but will not raise a warning from Biome.
*/
export type AnyFn = (...args: any[]) => any;
/**
* Type helper to extract non-function properties from a type.
*
* @remarks
* Useful to produce a type that is roughly the same as the type of `{... obj}`, where `obj` is an instance of `T`.
* A couple of differences:
* - Private and protected properties are not included.
* - Nested properties are not recursively extracted. For this, use {@linkcode NonFunctionPropertiesRecursive}
*/
export type NonFunctionProperties<T> = {
[K in keyof T as T[K] extends AnyFn ? never : K]: T[K];
};
/**
* Type helper to extract out non-function properties from a type, recursively applying to nested properties.
*/
export type NonFunctionPropertiesRecursive<Class> = {
[K in keyof Class as Class[K] extends AnyFn ? never : K]: Class[K] extends Array<infer U>
? NonFunctionPropertiesRecursive<U>[]
: Class[K] extends object
? NonFunctionPropertiesRecursive<Class[K]>
: Class[K];
};
export type AbstractConstructor<T> = abstract new (...args: any[]) => T;

View File

@ -50,7 +50,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { BiomeId } from "#enums/biome-id";
import { EaseType } from "#enums/ease-type";
import { ExpGainsSpeed } from "#enums/exp-gains-speed";
import type { ExpNotification } from "#enums/exp-notification";
import { ExpNotification } from "#enums/exp-notification";
import { FormChangeItem } from "#enums/form-change-item";
import { GameModes } from "#enums/game-modes";
import { ModifierPoolType } from "#enums/modifier-pool-type";
@ -197,6 +197,7 @@ export class BattleScene extends SceneBase {
public enableMoveInfo = true;
public enableRetries = false;
public hideIvs = false;
// TODO: Remove all plain numbers in place of enums or `const object` equivalents for clarity
/**
* Determines the condition for a notification should be shown for Candy Upgrades
* - 0 = 'Off'
@ -214,7 +215,7 @@ export class BattleScene extends SceneBase {
public uiTheme: UiTheme = UiTheme.DEFAULT;
public windowType = 0;
public experimentalSprites = false;
public musicPreference: number = MusicPreference.ALLGENS;
public musicPreference: MusicPreference = MusicPreference.ALLGENS;
public moveAnimations = true;
public expGainsSpeed: ExpGainsSpeed = ExpGainsSpeed.DEFAULT;
public skipSeenDialogues = false;
@ -225,33 +226,18 @@ export class BattleScene extends SceneBase {
* - 2 = Always (automatically skip animation when hatching 2 or more eggs)
*/
public eggSkipPreference = 0;
/**
* Defines the experience gain display mode.
*
* @remarks
* The `expParty` can have several modes:
* - `0` - Default: The normal experience gain display, nothing changed.
* - `1` - Level Up Notification: Displays the level up in the small frame instead of a message.
* - `2` - Skip: No level up frame nor message.
*
* Modes `1` and `2` are still compatible with stats display, level up, new move, etc.
* @default 0 - Uses the default normal experience gain display.
* Defines the {@linkcode ExpNotification | Experience gain display mode}.
* @defaultValue {@linkcode ExpNotification.DEFAULT}
*/
public expParty: ExpNotification = 0;
public expParty: ExpNotification = ExpNotification.DEFAULT;
public hpBarSpeed = 0;
public fusionPaletteSwaps = true;
public enableTouchControls = false;
public enableVibration = false;
public showBgmBar = true;
/**
* Determines the selected battle style.
* - 0 = 'Switch'
* - 1 = 'Set' - The option to switch the active pokemon at the start of a battle will not display.
*/
public battleStyle: number = BattleStyle.SWITCH;
/** Determines the selected battle style. */
public battleStyle: BattleStyle = BattleStyle.SWITCH;
/**
* Defines whether or not to show type effectiveness hints
* - true: No hints
@ -829,7 +815,8 @@ export class BattleScene extends SceneBase {
/**
* Returns an array of Pokemon on both sides of the battle - player first, then enemy.
* Does not actually check if the pokemon are on the field or not, and always has length 4 regardless of battle type.
* @param activeOnly - Whether to consider only active pokemon; default `false`
* @param activeOnly - Whether to consider only active pokemon (as described by {@linkcode Pokemon.isActive()}); default `false`.
* If `true`, will also remove all `null` values from the array.
* @returns An array of {@linkcode Pokemon}, as described above.
*/
public getField(activeOnly = false): Pokemon[] {
@ -842,9 +829,9 @@ export class BattleScene extends SceneBase {
}
/**
* Used in doubles battles to redirect moves from one pokemon to another when one faints or is removed from the field
* @param removedPokemon {@linkcode Pokemon} the pokemon that is being removed from the field (flee, faint), moves to be redirected FROM
* @param allyPokemon {@linkcode Pokemon} the pokemon that will have the moves be redirected TO
* Attempt to redirect a move in double battles from a fainted/removed Pokemon to its ally.
* @param removedPokemon - The {@linkcode Pokemon} having been removed from the field.
* @param allyPokemon - The {@linkcode Pokemon} allied with the removed Pokemon; will have moves redirected to it
*/
redirectPokemonMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
// failsafe: if not a double battle just return
@ -870,10 +857,10 @@ export class BattleScene extends SceneBase {
/**
* Returns the ModifierBar of this scene, which is declared private and therefore not accessible elsewhere
* @param isEnemy Whether to return the enemy's modifier bar
* @returns {ModifierBar}
* @param isEnemy - Whether to return the enemy modifier bar instead of the player bar; default `false`
* @returns The {@linkcode ModifierBar} for the given side of the field
*/
getModifierBar(isEnemy?: boolean): ModifierBar {
getModifierBar(isEnemy = false): ModifierBar {
return isEnemy ? this.enemyModifierBar : this.modifierBar;
}
@ -1475,10 +1462,12 @@ export class BattleScene extends SceneBase {
if (!waveIndex && lastBattle) {
const isNewBiome = this.isNewBiome(lastBattle);
/** Whether to reset and recall pokemon */
const resetArenaState =
isNewBiome ||
[BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.currentBattle.battleType) ||
this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS;
for (const enemyPokemon of this.getEnemyParty()) {
enemyPokemon.destroy();
}
@ -1853,7 +1842,7 @@ export class BattleScene extends SceneBase {
}
resetSeed(waveIndex?: number): void {
const wave = waveIndex || this.currentBattle?.waveIndex || 0;
const wave = waveIndex ?? this.currentBattle?.waveIndex ?? 0;
this.waveSeed = shiftCharCodes(this.seed, wave);
Phaser.Math.RND.sow([this.waveSeed]);
console.log("Wave Seed:", this.waveSeed, wave);

View File

@ -145,76 +145,141 @@ export class Ability implements Localizable {
return this.attrs.some(attr => attr instanceof targetAttr);
}
attr<T extends Constructor<AbAttr>>(AttrType: T, ...args: ConstructorParameters<T>): Ability {
/**
* Create a new {@linkcode AbAttr} instance and add it to this {@linkcode Ability}.
* @param attrType - The constructor of the {@linkcode AbAttr} to create.
* @param args - The arguments needed to instantiate the given class.
* @returns `this`
*/
attr<T extends Constructor<AbAttr>>(AttrType: T, ...args: ConstructorParameters<T>): this {
const attr = new AttrType(...args);
this.attrs.push(attr);
return this;
}
/**
* Create a new {@linkcode AbAttr} instance with the given condition and add it to this {@linkcode Ability}.
* Checked before all other conditions, and is unique to the individual {@linkcode AbAttr} being created.
* @param condition - The {@linkcode AbAttrCondition} to add.
* @param attrType - The constructor of the {@linkcode AbAttr} to create.
* @param args - The arguments needed to instantiate the given class.
* @returns `this`
*/
conditionalAttr<T extends Constructor<AbAttr>>(
condition: AbAttrCondition,
AttrType: T,
attrType: T,
...args: ConstructorParameters<T>
): Ability {
const attr = new AttrType(...args);
): this {
const attr = new attrType(...args);
attr.addCondition(condition);
this.attrs.push(attr);
return this;
}
bypassFaint(): Ability {
/**
* Make this ability trigger even if the user faints.
* @returns `this`
* @remarks
* This is also required for abilities to trigger when revived via Reviver Seed.
*/
bypassFaint(): this {
this.isBypassFaint = true;
return this;
}
ignorable(): Ability {
/**
* Make this ability ignorable by effects like {@linkcode MoveId.SUNSTEEL_STRIKE | Sunsteel Strike} or {@linkcode AbilityId.MOLD_BREAKER | Mold Breaker}.
* @returns `this`
*/
ignorable(): this {
this.isIgnorable = true;
return this;
}
unsuppressable(): Ability {
/**
* Make this ability unsuppressable by effects like {@linkcode MoveId.GASTRO_ACID | Gastro Acid} or {@linkcode AbilityId.NEUTRALIZING_GAS | Neutralizing Gas}.
* @returns `this`
*/
unsuppressable(): this {
this.isSuppressable = false;
return this;
}
uncopiable(): Ability {
/**
* Make this ability uncopiable by effects like {@linkcode MoveId.ROLE_PLAY | Role Play} or {@linkcode AbilityId.TRACE | Trace}.
* @returns `this`
*/
uncopiable(): this {
this.isCopiable = false;
return this;
}
unreplaceable(): Ability {
/**
* Make this ability unreplaceable by effects like {@linkcode MoveId.SIMPLE_BEAM | Simple Beam} or {@linkcode MoveId.ENTRAINMENT | Entrainment}.
* @returns `this`
*/
unreplaceable(): this {
this.isReplaceable = false;
return this;
}
condition(condition: AbAttrCondition): Ability {
/**
* Add a condition for this ability to be applied.
* Applies to **all** attributes of the given ability.
* @param condition - The {@linkcode AbAttrCondition} to add
* @returns `this`
* @see {@linkcode AbAttr.canApply} for setting conditions per attribute type
* @see {@linkcode conditionalAttr} for setting individual conditions per attribute instance
* @todo Review if this is necessary anymore - this is used extremely sparingly
*/
condition(condition: AbAttrCondition): this {
this.conditions.push(condition);
return this;
}
/**
* Mark an ability as partially implemented.
* Partial abilities are expected to have some of their core functionality implemented, but may lack
* certain notable features or interactions with other moves or abilities.
* @returns `this`
*/
partial(): this {
this.nameAppend += " (P)";
return this;
}
/**
* Mark an ability as unimplemented.
* Unimplemented abilities are ones which have _none_ of their basic functionality enabled.
* @returns `this`
*/
unimplemented(): this {
this.nameAppend += " (N)";
return this;
}
/**
* Internal flag used for developers to document edge cases. When using this, please be sure to document the edge case.
* @returns the ability
* Mark an ability as having one or more edge cases.
* It may lack certain niche interactions with other moves/abilities, but still functions
* as intended in most cases.
* Does not show up in game and is solely for internal dev use.
*
* When using this, make sure to **document the edge case** (or else this becomes pointless).
* @returns `this`
*/
edgeCase(): this {
return this;
}
}
/** Base set of parameters passed to every ability attribute's apply method */
/**
* Base set of parameters passed to every ability attribute's {@linkcode AbAttr.apply | apply} method.
*
* Extended by sub-classes to contain additional parameters pertaining to the ability type(s) being triggered.
*/
export interface AbAttrBaseParams {
/** The pokemon that has the ability being applied */
readonly pokemon: Pokemon;
@ -245,9 +310,20 @@ export interface AbAttrParamsWithCancel extends AbAttrBaseParams {
readonly cancelled: BooleanHolder;
}
/**
* Abstract class for all ability attributes.
*
* Each {@linkcode Ability} may have any number of individual attributes, each functioning independently from one another.
*/
export abstract class AbAttr {
public showAbility: boolean;
private extraCondition: AbAttrCondition;
/**
* Whether to show this ability as a flyout when applying its effects.
* Should be kept in parity with mainline where possible.
* @defaultValue `true`
*/
public showAbility = true;
/** The additional condition associated with this AbAttr, if any. */
private extraCondition?: AbAttrCondition;
/**
* Return whether this attribute is of the given type.
@ -275,21 +351,43 @@ export abstract class AbAttr {
}
/**
* Apply ability effects without checking conditions.
* **Never call this method directly, use {@linkcode applyAbAttrs} instead.**
* Apply this attribute's effects without checking conditions.
*
* @remarks
* **Never call this method directly!** \
* Use {@linkcode applyAbAttrs} instead.
*/
apply(_params: AbAttrBaseParams): void {}
// The `Exact` in the next two signatures enforces that the type of the _params operand
// is always compatible with the type of apply. This allows fewer fields, but never a type with more.
/**
* Return the trigger message to show when this attribute is executed.
* @param _params - The parameters passed to this attribute's {@linkcode apply} function; must match type exactly
* @param _abilityName - The name of the current ability.
* @privateRemarks
* If more fields are provided than needed, any excess can be discarded using destructuring.
* @todo Remove `null` from signature in lieu of using an empty string
*/
getTriggerMessage(_params: Exact<Parameters<this["apply"]>[0]>, _abilityName: string): string | null {
return null;
}
/**
* Check whether this attribute can have its effects successfully applied.
* Applies to **all** instances of the given attribute.
* @param _params - The parameters passed to this attribute's {@linkcode apply} function; must match type exactly
* @privateRemarks
* If more fields are provided than needed, any excess can be discarded using destructuring.
*/
canApply(_params: Exact<Parameters<this["apply"]>[0]>): boolean {
return true;
}
/**
* Return the additional condition associated with this particular AbAttr instance, if any.
* @returns The extra condition for this {@linkcode AbAttr}, or `null` if none exist
* @todo Make this use `undefined` instead of `null`
* @todo Prevent this from being overridden by sub-classes
*/
getCondition(): AbAttrCondition | null {
return this.extraCondition || null;
}
@ -593,7 +691,7 @@ export class TypeImmunityAbAttr extends PreDefendAbAttr {
private immuneType: PokemonType | null;
private condition: AbAttrCondition | null;
// TODO: `immuneType` shouldn't be able to be `null`
// TODO: Change `NonSuperEffectiveImmunityAbAttr` to not pass `null` as immune type
constructor(immuneType: PokemonType | null, condition?: AbAttrCondition) {
super(true);
@ -1526,6 +1624,11 @@ export interface FieldMultiplyStatAbAttrParams extends AbAttrBaseParams {
export class FieldMultiplyStatAbAttr extends AbAttr {
private stat: Stat;
private multiplier: number;
/**
* Whether this ability can stack with others of the same type for this stat.
* @defaultValue `false`
* @todo Remove due to being literally useless - the ruin abilities are hardcoded to never stack in game
*/
private canStack: boolean;
constructor(stat: Stat, multiplier: number, canStack = false) {
@ -1546,7 +1649,7 @@ export class FieldMultiplyStatAbAttr extends AbAttr {
}
/**
* applyFieldStat: Tries to multiply a Pokemon's Stat
* Atttempt to multiply a Pokemon's Stat.
*/
apply({ statVal, hasApplied }: FieldMultiplyStatAbAttrParams): void {
statVal.value *= this.multiplier;
@ -1572,23 +1675,25 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
}
/**
* Determine if the move type change attribute can be applied
* Determine if the move type change attribute can be applied.
*
* Can be applied if:
* - The ability's condition is met, e.g. pixilate only boosts normal moves,
* - The move is not forbidden from having its type changed by an ability, e.g. {@linkcode MoveId.MULTI_ATTACK}
* - The user is not terastallized and using tera blast
* - The user is not a terastallized terapagos with tera stellar using tera starstorm
* - The user is not Terastallized and using Tera Blast
* - The user is not a Terastallized Terapagos using Stellar-type Tera Starstorm
*/
override canApply({ pokemon, opponent: target, move }: MoveTypeChangeAbAttrParams): boolean {
return (
(!this.condition || this.condition(pokemon, target, move)) &&
!noAbilityTypeOverrideMoves.has(move.id) &&
(!pokemon.isTerastallized ||
(move.id !== MoveId.TERA_BLAST &&
(move.id !== MoveId.TERA_STARSTORM ||
pokemon.getTeraType() !== PokemonType.STELLAR ||
!pokemon.hasSpecies(SpeciesId.TERAPAGOS))))
!(
pokemon.isTerastallized &&
(move.id === MoveId.TERA_BLAST ||
(move.id === MoveId.TERA_STARSTORM &&
pokemon.getTeraType() === PokemonType.STELLAR &&
pokemon.hasSpecies(SpeciesId.TERAPAGOS)))
)
);
}
@ -1661,23 +1766,19 @@ export interface AddSecondStrikeAbAttrParams extends Omit<AugmentMoveInteraction
}
/**
* Class for abilities that convert single-strike moves to two-strike moves (i.e. Parental Bond).
* @param damageMultiplier the damage multiplier for the second strike, relative to the first.
* Class for abilities that add additional strikes to single-target moves.
* Used by {@linkcode Moves.PARENTAL_BOND | Parental Bond}.
*/
export class AddSecondStrikeAbAttr extends PreAttackAbAttr {
private damageMultiplier: number;
/**
* @param damageMultiplier - The damage multiplier for the second strike, relative to the first
*/
constructor(damageMultiplier: number) {
constructor(private damageMultiplier: number) {
super(false);
this.damageMultiplier = damageMultiplier;
}
/**
* Return whether the move can be multi-strike enhanced
* Return whether the move can be multi-strike enhanced.
*/
override canApply({ pokemon, move }: AddSecondStrikeAbAttrParams): boolean {
return move.canBeMultiStrikeEnhanced(pokemon, true);
@ -2021,9 +2122,10 @@ export abstract class PostAttackAbAttr extends AbAttr {
}
/**
* By default, this method checks that the move used is a damaging attack.
* This can be changed by providing a different {@link attackCondition} to the constructor.
* @see {@link ConfusionOnStatusEffectAbAttr} for an example of an effect that does not require a damaging move.
* By default, this method checks that the move used is a damaging attack before
* applying the effect of any inherited class.
* This can be changed by providing a different {@linkcode attackCondition} to the constructor.
* @see {@linkcode ConfusionOnStatusEffectAbAttr} for an example of an effect that does not require a damaging move.
*/
override canApply({ pokemon, opponent, move }: Closed<PostMoveInteractionAbAttrParams>): boolean {
return this.attackCondition(pokemon, opponent, move);
@ -3511,18 +3613,18 @@ export interface ConfusionOnStatusEffectAbAttrParams extends AbAttrBaseParams {
*/
export class ConfusionOnStatusEffectAbAttr extends AbAttr {
/** List of effects to apply confusion after */
private effects: StatusEffect[];
private effects: ReadonlySet<StatusEffect>;
constructor(...effects: StatusEffect[]) {
super();
this.effects = effects;
this.effects = new Set(effects);
}
/**
* @returns Whether the ability can apply confusion to the opponent
*/
override canApply({ opponent, effect }: ConfusionOnStatusEffectAbAttrParams): boolean {
return this.effects.includes(effect) && !opponent.isFainted() && opponent.canAddTag(BattlerTagType.CONFUSED);
return this.effects.has(effect) && !opponent.isFainted() && opponent.canAddTag(BattlerTagType.CONFUSED);
}
/**
@ -4500,8 +4602,8 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr {
}
/**
* After the turn ends, resets the status of either the ability holder or their ally
* @param allyTarget Whether to target ally, defaults to false (self-target)
* After the turn ends, resets the status of either the user or their ally.
* @param allyTarget Whether to target the user's ally; default `false` (self-target)
*
* @sealed
*/
@ -4786,17 +4888,22 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr {
!opp.switchOutStatus,
);
}
/** Deals damage to all sleeping opponents equal to 1/8 of their max hp (min 1) */
/** Deal damage to all sleeping, on-field opponents equal to 1/8 of their max hp (min 1). */
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
if (simulated) {
return;
}
for (const opp of pokemon.getOpponents()) {
if (
(opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(AbilityId.COMATOSE)) &&
!opp.hasAbilityWithAttr("BlockNonDirectDamageAbAttr") &&
!opp.switchOutStatus
) {
if ((opp.status?.effect !== StatusEffect.SLEEP && !opp.hasAbility(AbilityId.COMATOSE)) || opp.switchOutStatus) {
continue;
}
const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, simulated, cancelled });
if (!cancelled.value) {
opp.damageAndUpdate(toDmgValue(opp.getMaxHp() / 8), { result: HitResult.INDIRECT });
globalScene.phaseManager.queueMessage(
i18next.t("abilityTriggers:badDreams", { pokemonName: getPokemonNameWithAffix(opp) }),
@ -4809,7 +4916,8 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr {
/**
* Grabs the last failed Pokeball used
* @sealed
* @see {@linkcode applyPostTurn} */
* @see {@linkcode applyPostTurn}
*/
export class FetchBallAbAttr extends PostTurnAbAttr {
override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean {
return !simulated && !isNullOrUndefined(globalScene.currentBattle.lastUsedPokeball) && !!pokemon.isPlayer;
@ -5681,11 +5789,13 @@ export class InfiltratorAbAttr extends AbAttr {
* Allows the source to bounce back {@linkcode MoveFlags.REFLECTABLE | Reflectable}
* moves as if the user had used {@linkcode MoveId.MAGIC_COAT | Magic Coat}.
* @sealed
* @todo Make reflection a part of this ability's effects
*/
export class ReflectStatusMoveAbAttr extends AbAttr {
private declare readonly _: never;
}
// TODO: Make these ability attributes be flags instead of dummy attributes
/** @sealed */
export class NoTransformAbilityAbAttr extends AbAttr {
private declare readonly _: never;

View File

@ -8,23 +8,18 @@ function applySingleAbAttrs<T extends AbAttrString>(
messages: string[] = [],
) {
const { simulated = false, passive = false, pokemon } = params;
if (!pokemon?.canApplyAbility(passive) || (passive && pokemon.getPassiveAbility().id === pokemon.getAbility().id)) {
if (!pokemon.canApplyAbility(passive) || (passive && pokemon.getPassiveAbility().id === pokemon.getAbility().id)) {
return;
}
const ability = passive ? pokemon.getPassiveAbility() : pokemon.getAbility();
if (
gainedMidTurn &&
ability.getAttrs(attrType).some(attr => {
attr.is("PostSummonAbAttr") && !attr.shouldActivateOnGain();
})
) {
const attrs = ability.getAttrs(attrType);
if (gainedMidTurn && attrs.some(attr => attr.is("PostSummonAbAttr") && !attr.shouldActivateOnGain())) {
return;
}
for (const attr of ability.getAttrs(attrType)) {
for (const attr of attrs) {
const condition = attr.getCondition();
let abShown = false;
// We require an `as any` cast to suppress an error about the `params` type not being assignable to
// the type of the argument expected by `attr.canApply()`. This is OK, because we know that
// `attr` is an instance of the `attrType` class provided to the method, and typescript _will_ check
@ -33,7 +28,7 @@ function applySingleAbAttrs<T extends AbAttrString>(
continue;
}
globalScene.phaseManager.setPhaseQueueSplice();
let abShown = false;
if (attr.showAbility && !simulated) {
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true);
@ -45,6 +40,7 @@ function applySingleAbAttrs<T extends AbAttrString>(
if (!simulated) {
globalScene.phaseManager.queueMessage(message);
}
// TODO: Should messages be added to the array if they aren't actually shown?
messages.push(message);
}
// The `as any` cast here uses the same reasoning as above.
@ -57,8 +53,6 @@ function applySingleAbAttrs<T extends AbAttrString>(
if (!simulated) {
pokemon.waveData.abilitiesApplied.add(ability.id);
}
globalScene.phaseManager.clearPhaseQueueSplice();
}
}

View File

@ -19,22 +19,88 @@ import { Stat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import type { Arena } from "#field/arena";
import type { Pokemon } from "#field/pokemon";
import type {
ArenaDelayedAttackTagType,
ArenaScreenTagType,
ArenaTagTypeData,
ArenaTrapTagType,
SerializableArenaTagType,
} from "#types/arena-tags";
import type { Mutable, NonFunctionProperties } from "#types/type-helpers";
import { BooleanHolder, NumberHolder, toDmgValue } from "#utils/common";
import i18next from "i18next";
/*
ArenaTags are are meant for effects that are tied to the arena (as opposed to a specific pokemon).
Examples include (but are not limited to)
- Cross-turn effects that persist even if the user/target switches out, such as Wish, Future Sight, and Happy Hour
- Effects that are applied to a specific side of the field, such as Crafty Shield, Reflect, and Spikes
- Field-Effects, like Gravity and Trick Room
Any arena tag that persists across turns *must* extend from `SerializableArenaTag` in the class definition signature.
Serializable ArenaTags have strict rules for their fields.
These rules ensure that only the data necessary to reconstruct the tag is serialized, and that the
session loader is able to deserialize saved tags correctly.
If the data is static (i.e. it is always the same for all instances of the class, such as the
type that is weakened by Mud Sport/Water Sport), then it must not be defined as a field, and must
instead be defined as a getter.
A static property is also acceptable, though static properties are less ergonomic with inheritance.
If the data is mutable (i.e. it can change over the course of the tag's lifetime), then it *must*
be defined as a field, and it must be set in the `loadTag` method.
Such fields cannot be marked as `private/protected`, as if they were, typescript would omit them from
types that are based off of the class, namely, `ArenaTagTypeData`. It is preferrable to trade the
type-safety of private/protected fields for the type safety when deserializing arena tags from save data.
For data that is mutable only within a turn (e.g. SuppressAbilitiesTag's beingRemoved field),
where it does not make sense to be serialized, the field should use ES2020's private field syntax (a `#` prepended to the field name).
If the field should be accessible outside of the class, then a public getter should be used.
*/
/** Interface containing the serializable fields of ArenaTagData. */
interface BaseArenaTag {
/**
* The tag's remaining duration. Setting to any number `<=0` will make the tag's duration effectively infinite.
*/
turnCount: number;
/**
* The {@linkcode MoveId} that created this tag, or `undefined` if not set by a move.
*/
sourceMove?: MoveId;
/**
* The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} having created the tag, or `undefined` if not set by a Pokemon.
* @todo Implement handling for `ArenaTag`s created by non-pokemon sources (most tags will throw errors without a source)
*/
// Note: Intentionally not using `?`, as the property should always exist, but just be undefined if not present.
sourceId: number | undefined;
/**
* The {@linkcode ArenaTagSide | side of the field} that this arena tag affects.
* @defaultValue `ArenaTagSide.BOTH`
*/
side: ArenaTagSide;
}
/**
* An {@linkcode ArenaTag} represents a semi-persistent effect affecting a given side of the field.
* An {@linkcode ArenaTag} represents a semi-persistent effect affecting a given _side_ of the field.
* Unlike {@linkcode BattlerTag}s (which are tied to individual {@linkcode Pokemon}), `ArenaTag`s function independently of
* the Pokemon currently on-field, only cleared on arena reset or through their respective {@linkcode ArenaTag.lapse | lapse} methods.
*/
export abstract class ArenaTag {
constructor(
public tagType: ArenaTagType,
public turnCount: number,
public sourceMove?: MoveId,
public sourceId?: number,
public side: ArenaTagSide = ArenaTagSide.BOTH,
) {}
export abstract class ArenaTag implements BaseArenaTag {
/** The type of the arena tag */
public abstract readonly tagType: ArenaTagType;
public turnCount: number;
public sourceMove?: MoveId;
public sourceId: number | undefined;
public side: ArenaTagSide;
constructor(turnCount: number, sourceMove?: MoveId, sourceId?: number, side: ArenaTagSide = ArenaTagSide.BOTH) {
this.turnCount = turnCount;
this.sourceMove = sourceMove;
this.sourceId = sourceId;
this.side = side;
}
apply(_arena: Arena, _simulated: boolean, ..._args: unknown[]): boolean {
return true;
@ -55,8 +121,17 @@ export abstract class ArenaTag {
onOverlap(_arena: Arena, _source: Pokemon | null): void {}
/**
* Trigger this {@linkcode ArenaTag}'s effect, reducing its duration as applicable.
* Will ignore durations of all tags with durations `<=0`.
* @param _arena - The {@linkcode Arena} at the moment the tag is being lapsed.
* Unused by default but can be used by sub-classes.
* @returns `true` if this tag should be kept; `false` if it should be removed.
*/
lapse(_arena: Arena): boolean {
return this.turnCount < 1 || !!--this.turnCount;
// TODO: Rather than treating negative duration tags as being indefinite,
// make all duration based classes inherit from their own sub-class
return this.turnCount < 1 || --this.turnCount > 0;
}
getMoveName(): string | null {
@ -66,9 +141,9 @@ export abstract class ArenaTag {
/**
* When given a arena tag or json representing one, load the data for it.
* This is meant to be inherited from by any arena tag with custom attributes
* @param {ArenaTag | any} source An arena tag
* @param source - The {@linkcode BaseArenaTag} being loaded
*/
loadTag(source: ArenaTag | any): void {
loadTag(source: BaseArenaTag): void {
this.turnCount = source.turnCount;
this.sourceMove = source.sourceMove;
this.sourceId = source.sourceId;
@ -101,13 +176,21 @@ export abstract class ArenaTag {
}
}
/**
* Abstract class for arena tags that can persist across turns.
*/
export abstract class SerializableArenaTag extends ArenaTag {
abstract readonly tagType: SerializableArenaTagType;
}
/**
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Mist_(move) Mist}.
* Prevents Pokémon on the opposing side from lowering the stats of the Pokémon in the Mist.
*/
export class MistTag extends ArenaTag {
constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.MIST, turnCount, MoveId.MIST, sourceId, side);
export class MistTag extends SerializableArenaTag {
readonly tagType = ArenaTagType.MIST;
constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) {
super(turnCount, MoveId.MIST, sourceId, side);
}
onAdd(arena: Arena, quiet = false): void {
@ -164,33 +247,11 @@ export class MistTag extends ArenaTag {
/**
* Reduces the damage of specific move categories in the arena.
* @extends ArenaTag
*/
export class WeakenMoveScreenTag extends ArenaTag {
protected weakenedCategories: MoveCategory[];
/**
* Creates a new instance of the WeakenMoveScreenTag class.
*
* @param tagType - The type of the arena tag.
* @param turnCount - The number of turns the tag is active.
* @param sourceMove - The move that created the tag.
* @param sourceId - The ID of the source of the tag.
* @param side - The side (player or enemy) the tag affects.
* @param weakenedCategories - The categories of moves that are weakened by this tag.
*/
constructor(
tagType: ArenaTagType,
turnCount: number,
sourceMove: MoveId,
sourceId: number,
side: ArenaTagSide,
weakenedCategories: MoveCategory[],
) {
super(tagType, turnCount, sourceMove, sourceId, side);
this.weakenedCategories = weakenedCategories;
}
export abstract class WeakenMoveScreenTag extends SerializableArenaTag {
public abstract readonly tagType: ArenaScreenTagType;
// Getter to avoid unnecessary serialization and prevent modification
protected abstract get weakenedCategories(): MoveCategory[];
/**
* Applies the weakening effect to the move.
@ -227,8 +288,13 @@ export class WeakenMoveScreenTag extends ArenaTag {
* Used by {@linkcode MoveId.REFLECT}
*/
class ReflectTag extends WeakenMoveScreenTag {
constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.REFLECT, turnCount, MoveId.REFLECT, sourceId, side, [MoveCategory.PHYSICAL]);
public readonly tagType = ArenaTagType.REFLECT;
protected get weakenedCategories(): [MoveCategory.PHYSICAL] {
return [MoveCategory.PHYSICAL];
}
constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) {
super(turnCount, MoveId.REFLECT, sourceId, side);
}
onAdd(_arena: Arena, quiet = false): void {
@ -247,8 +313,12 @@ class ReflectTag extends WeakenMoveScreenTag {
* Used by {@linkcode MoveId.LIGHT_SCREEN}
*/
class LightScreenTag extends WeakenMoveScreenTag {
constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.LIGHT_SCREEN, turnCount, MoveId.LIGHT_SCREEN, sourceId, side, [MoveCategory.SPECIAL]);
public readonly tagType = ArenaTagType.LIGHT_SCREEN;
protected get weakenedCategories(): [MoveCategory.SPECIAL] {
return [MoveCategory.SPECIAL];
}
constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) {
super(turnCount, MoveId.LIGHT_SCREEN, sourceId, side);
}
onAdd(_arena: Arena, quiet = false): void {
@ -267,11 +337,13 @@ class LightScreenTag extends WeakenMoveScreenTag {
* Used by {@linkcode MoveId.AURORA_VEIL}
*/
class AuroraVeilTag extends WeakenMoveScreenTag {
constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.AURORA_VEIL, turnCount, MoveId.AURORA_VEIL, sourceId, side, [
MoveCategory.SPECIAL,
MoveCategory.PHYSICAL,
]);
public readonly tagType = ArenaTagType.AURORA_VEIL;
protected get weakenedCategories(): [MoveCategory.PHYSICAL, MoveCategory.SPECIAL] {
return [MoveCategory.PHYSICAL, MoveCategory.SPECIAL];
}
constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) {
super(turnCount, MoveId.AURORA_VEIL, sourceId, side);
}
onAdd(_arena: Arena, quiet = false): void {
@ -291,21 +363,23 @@ type ProtectConditionFunc = (arena: Arena, moveId: MoveId) => boolean;
* Class to implement conditional team protection
* applies protection based on the attributes of incoming moves
*/
export class ConditionalProtectTag extends ArenaTag {
export abstract class ConditionalProtectTag extends ArenaTag {
/** The condition function to determine which moves are negated */
protected protectConditionFunc: ProtectConditionFunc;
/** Does this apply to all moves, including those that ignore other forms of protection? */
/**
* Whether this protection effect should apply to _all_ moves, including ones that ignore other forms of protection.
* @defaultValue `false`
*/
protected ignoresBypass: boolean;
constructor(
tagType: ArenaTagType,
sourceMove: MoveId,
sourceId: number,
sourceId: number | undefined,
side: ArenaTagSide,
condition: ProtectConditionFunc,
ignoresBypass = false,
) {
super(tagType, 1, sourceMove, sourceId, side);
super(1, sourceMove, sourceId, side);
this.protectConditionFunc = condition;
this.ignoresBypass = ignoresBypass;
@ -391,8 +465,9 @@ const QuickGuardConditionFunc: ProtectConditionFunc = (_arena, moveId) => {
* Condition: The incoming move has increased priority.
*/
class QuickGuardTag extends ConditionalProtectTag {
constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.QUICK_GUARD, MoveId.QUICK_GUARD, sourceId, side, QuickGuardConditionFunc);
public readonly tagType = ArenaTagType.QUICK_GUARD;
constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.QUICK_GUARD, sourceId, side, QuickGuardConditionFunc);
}
}
@ -422,8 +497,9 @@ const WideGuardConditionFunc: ProtectConditionFunc = (_arena, moveId): boolean =
* can be an ally or enemy.
*/
class WideGuardTag extends ConditionalProtectTag {
constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.WIDE_GUARD, MoveId.WIDE_GUARD, sourceId, side, WideGuardConditionFunc);
public readonly tagType = ArenaTagType.WIDE_GUARD;
constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.WIDE_GUARD, sourceId, side, WideGuardConditionFunc);
}
}
@ -444,8 +520,9 @@ const MatBlockConditionFunc: ProtectConditionFunc = (_arena, moveId): boolean =>
* Condition: The incoming move is a Physical or Special attack move.
*/
class MatBlockTag extends ConditionalProtectTag {
constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.MAT_BLOCK, MoveId.MAT_BLOCK, sourceId, side, MatBlockConditionFunc);
public readonly tagType = ArenaTagType.MAT_BLOCK;
constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.MAT_BLOCK, sourceId, side, MatBlockConditionFunc);
}
onAdd(_arena: Arena) {
@ -488,8 +565,9 @@ const CraftyShieldConditionFunc: ProtectConditionFunc = (_arena, moveId) => {
* not target all Pokemon or sides of the field.
*/
class CraftyShieldTag extends ConditionalProtectTag {
constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.CRAFTY_SHIELD, MoveId.CRAFTY_SHIELD, sourceId, side, CraftyShieldConditionFunc, true);
public readonly tagType = ArenaTagType.CRAFTY_SHIELD;
constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.CRAFTY_SHIELD, sourceId, side, CraftyShieldConditionFunc, true);
}
}
@ -497,17 +575,8 @@ class CraftyShieldTag extends ConditionalProtectTag {
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Lucky_Chant_(move) Lucky Chant}.
* Prevents critical hits against the tag's side.
*/
export class NoCritTag extends ArenaTag {
/**
* Constructor method for the NoCritTag class
* @param turnCount `number` the number of turns this effect lasts
* @param sourceMove {@linkcode MoveId} the move that created this effect
* @param sourceId `number` the ID of the {@linkcode Pokemon} that created this effect
* @param side {@linkcode ArenaTagSide} the side to which this effect belongs
*/
constructor(turnCount: number, sourceMove: MoveId, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.NO_CRIT, turnCount, sourceMove, sourceId, side);
}
export class NoCritTag extends SerializableArenaTag {
public readonly tagType = ArenaTagType.NO_CRIT;
/** Queues a message upon adding this effect to the field */
onAdd(_arena: Arena): void {
@ -538,23 +607,9 @@ export class NoCritTag extends ArenaTag {
/**
* Abstract class to implement weakened moves of a specific type.
*/
export class WeakenMoveTypeTag extends ArenaTag {
private weakenedType: PokemonType;
/**
* Creates a new instance of the WeakenMoveTypeTag class.
*
* @param tagType - The type of the arena tag.
* @param turnCount - The number of turns the tag is active.
* @param type - The type being weakened from this tag.
* @param sourceMove - The move that created the tag.
* @param sourceId - The ID of the source of the tag.
*/
constructor(tagType: ArenaTagType, turnCount: number, type: PokemonType, sourceMove: MoveId, sourceId: number) {
super(tagType, turnCount, sourceMove, sourceId);
this.weakenedType = type;
}
export abstract class WeakenMoveTypeTag extends SerializableArenaTag {
abstract readonly tagType: ArenaTagType.MUD_SPORT | ArenaTagType.WATER_SPORT;
abstract get weakenedType(): PokemonType;
/**
* Reduces an attack's power by 0.33x if it matches this tag's weakened type.
@ -578,8 +633,12 @@ export class WeakenMoveTypeTag extends ArenaTag {
* Weakens Electric type moves for a set amount of turns, usually 5.
*/
class MudSportTag extends WeakenMoveTypeTag {
constructor(turnCount: number, sourceId: number) {
super(ArenaTagType.MUD_SPORT, turnCount, PokemonType.ELECTRIC, MoveId.MUD_SPORT, sourceId);
public readonly tagType = ArenaTagType.MUD_SPORT;
override get weakenedType(): PokemonType.ELECTRIC {
return PokemonType.ELECTRIC;
}
constructor(turnCount: number, sourceId?: number) {
super(turnCount, MoveId.MUD_SPORT, sourceId);
}
onAdd(_arena: Arena): void {
@ -596,8 +655,12 @@ class MudSportTag extends WeakenMoveTypeTag {
* Weakens Fire type moves for a set amount of turns, usually 5.
*/
class WaterSportTag extends WeakenMoveTypeTag {
constructor(turnCount: number, sourceId: number) {
super(ArenaTagType.WATER_SPORT, turnCount, PokemonType.FIRE, MoveId.WATER_SPORT, sourceId);
public readonly tagType = ArenaTagType.WATER_SPORT;
override get weakenedType(): PokemonType.FIRE {
return PokemonType.FIRE;
}
constructor(turnCount: number, sourceId?: number) {
super(turnCount, MoveId.WATER_SPORT, sourceId);
}
onAdd(_arena: Arena): void {
@ -615,8 +678,9 @@ class WaterSportTag extends WeakenMoveTypeTag {
* Converts Normal-type moves to Electric type for the rest of the turn.
*/
export class IonDelugeTag extends ArenaTag {
public readonly tagType = ArenaTagType.ION_DELUGE;
constructor(sourceMove?: MoveId) {
super(ArenaTagType.ION_DELUGE, 1, sourceMove);
super(1, sourceMove);
}
/** Queues an on-add message */
@ -645,7 +709,8 @@ export class IonDelugeTag extends ArenaTag {
/**
* Abstract class to implement arena traps.
*/
export class ArenaTrapTag extends ArenaTag {
export abstract class ArenaTrapTag extends SerializableArenaTag {
abstract readonly tagType: ArenaTrapTagType;
public layers: number;
public maxLayers: number;
@ -658,8 +723,8 @@ export class ArenaTrapTag extends ArenaTag {
* @param side - The side (player or enemy) the tag affects.
* @param maxLayers - The maximum amount of layers this tag can have.
*/
constructor(tagType: ArenaTagType, sourceMove: MoveId, sourceId: number, side: ArenaTagSide, maxLayers: number) {
super(tagType, 0, sourceMove, sourceId, side);
constructor(sourceMove: MoveId, sourceId: number | undefined, side: ArenaTagSide, maxLayers: number) {
super(0, sourceMove, sourceId, side);
this.layers = 1;
this.maxLayers = maxLayers;
@ -698,7 +763,7 @@ export class ArenaTrapTag extends ArenaTag {
: Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2);
}
loadTag(source: any): void {
loadTag(source: NonFunctionProperties<ArenaTrapTag>): void {
super.loadTag(source);
this.layers = source.layers;
this.maxLayers = source.maxLayers;
@ -711,8 +776,9 @@ export class ArenaTrapTag extends ArenaTag {
* in damage for 1, 2, or 3 layers of Spikes respectively if they are summoned into this trap.
*/
class SpikesTag extends ArenaTrapTag {
constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.SPIKES, MoveId.SPIKES, sourceId, side, 3);
public readonly tagType = ArenaTagType.SPIKES;
constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.SPIKES, sourceId, side, 3);
}
onAdd(arena: Arena, quiet = false): void {
@ -769,11 +835,12 @@ class SpikesTag extends ArenaTrapTag {
* Pokémon summoned into this trap remove it entirely.
*/
class ToxicSpikesTag extends ArenaTrapTag {
private neutralized: boolean;
#neutralized: boolean;
public readonly tagType = ArenaTagType.TOXIC_SPIKES;
constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.TOXIC_SPIKES, MoveId.TOXIC_SPIKES, sourceId, side, 2);
this.neutralized = false;
constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.TOXIC_SPIKES, sourceId, side, 2);
this.#neutralized = false;
}
onAdd(arena: Arena, quiet = false): void {
@ -799,7 +866,7 @@ class ToxicSpikesTag extends ArenaTrapTag {
}
onRemove(arena: Arena): void {
if (!this.neutralized) {
if (!this.#neutralized) {
super.onRemove(arena);
}
}
@ -810,7 +877,7 @@ class ToxicSpikesTag extends ArenaTrapTag {
return true;
}
if (pokemon.isOfType(PokemonType.POISON)) {
this.neutralized = true;
this.#neutralized = true;
if (globalScene.arena.removeTag(this.tagType)) {
globalScene.phaseManager.queueMessage(
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
@ -850,8 +917,9 @@ class ToxicSpikesTag extends ArenaTrapTag {
* who is summoned into the trap, based on the Rock type's type effectiveness.
*/
class StealthRockTag extends ArenaTrapTag {
constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.STEALTH_ROCK, MoveId.STEALTH_ROCK, sourceId, side, 1);
public readonly tagType = ArenaTagType.STEALTH_ROCK;
constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.STEALTH_ROCK, sourceId, side, 1);
}
onAdd(arena: Arena, quiet = false): void {
@ -939,8 +1007,9 @@ class StealthRockTag extends ArenaTrapTag {
* to any Pokémon who is summoned into this trap.
*/
class StickyWebTag extends ArenaTrapTag {
constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.STICKY_WEB, MoveId.STICKY_WEB, sourceId, side, 1);
public readonly tagType = ArenaTagType.STICKY_WEB;
constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.STICKY_WEB, sourceId, side, 1);
}
onAdd(arena: Arena, quiet = false): void {
@ -1007,14 +1076,57 @@ class StickyWebTag extends ArenaTrapTag {
}
}
/**
* Arena Tag class for delayed attacks, such as {@linkcode MoveId.FUTURE_SIGHT} or {@linkcode MoveId.DOOM_DESIRE}.
* Delays the attack's effect by a set amount of turns, usually 3 (including the turn the move is used),
* and deals damage after the turn count is reached.
*/
export class DelayedAttackTag extends SerializableArenaTag {
public targetIndex: BattlerIndex;
public readonly tagType: ArenaDelayedAttackTagType;
constructor(
tagType: ArenaTagType.DOOM_DESIRE | ArenaTagType.FUTURE_SIGHT,
sourceMove: MoveId | undefined,
sourceId: number | undefined,
targetIndex: BattlerIndex,
side: ArenaTagSide = ArenaTagSide.BOTH,
) {
super(3, sourceMove, sourceId, side);
this.tagType = tagType;
this.targetIndex = targetIndex;
this.side = side;
}
lapse(arena: Arena): boolean {
const ret = super.lapse(arena);
if (!ret) {
// TODO: This should not add to move history (for Spite)
globalScene.phaseManager.unshiftNew(
"MoveEffectPhase",
this.sourceId!,
[this.targetIndex],
allMoves[this.sourceMove!],
MoveUseMode.FOLLOW_UP,
); // TODO: are those bangs correct?
}
return ret;
}
onRemove(_arena: Arena): void {}
}
/**
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Trick_Room_(move) Trick Room}.
* Reverses the Speed stats for all Pokémon on the field as long as this arena tag is up,
* also reversing the turn order for all Pokémon on the field as well.
*/
export class TrickRoomTag extends ArenaTag {
constructor(turnCount: number, sourceId: number) {
super(ArenaTagType.TRICK_ROOM, turnCount, MoveId.TRICK_ROOM, sourceId);
export class TrickRoomTag extends SerializableArenaTag {
public readonly tagType = ArenaTagType.TRICK_ROOM;
constructor(turnCount: number, sourceId?: number) {
super(turnCount, MoveId.TRICK_ROOM, sourceId);
}
/**
@ -1056,9 +1168,10 @@ export class TrickRoomTag extends ArenaTag {
* Grounds all Pokémon on the field, including Flying-types and those with
* {@linkcode AbilityId.LEVITATE} for the duration of the arena tag, usually 5 turns.
*/
export class GravityTag extends ArenaTag {
constructor(turnCount: number) {
super(ArenaTagType.GRAVITY, turnCount, MoveId.GRAVITY);
export class GravityTag extends SerializableArenaTag {
public readonly tagType = ArenaTagType.GRAVITY;
constructor(turnCount: number, sourceId?: number) {
super(turnCount, MoveId.GRAVITY, sourceId);
}
onAdd(_arena: Arena): void {
@ -1084,9 +1197,10 @@ export class GravityTag extends ArenaTag {
* Doubles the Speed of the Pokémon who created this arena tag, as well as all allied Pokémon.
* Applies this arena tag for 4 turns (including the turn the move was used).
*/
class TailwindTag extends ArenaTag {
constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.TAILWIND, turnCount, MoveId.TAILWIND, sourceId, side);
class TailwindTag extends SerializableArenaTag {
public readonly tagType = ArenaTagType.TAILWIND;
constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) {
super(turnCount, MoveId.TAILWIND, sourceId, side);
}
onAdd(_arena: Arena, quiet = false): void {
@ -1152,9 +1266,10 @@ class TailwindTag extends ArenaTag {
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Happy_Hour_(move) Happy Hour}.
* Doubles the prize money from trainers and money moves like {@linkcode MoveId.PAY_DAY} and {@linkcode MoveId.MAKE_IT_RAIN}.
*/
class HappyHourTag extends ArenaTag {
constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.HAPPY_HOUR, turnCount, MoveId.HAPPY_HOUR, sourceId, side);
class HappyHourTag extends SerializableArenaTag {
public readonly tagType = ArenaTagType.HAPPY_HOUR;
constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) {
super(turnCount, MoveId.HAPPY_HOUR, sourceId, side);
}
onAdd(_arena: Arena): void {
@ -1167,8 +1282,9 @@ class HappyHourTag extends ArenaTag {
}
class SafeguardTag extends ArenaTag {
constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.SAFEGUARD, turnCount, MoveId.SAFEGUARD, sourceId, side);
public readonly tagType = ArenaTagType.SAFEGUARD;
constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) {
super(turnCount, MoveId.SAFEGUARD, sourceId, side);
}
onAdd(_arena: Arena): void {
@ -1189,18 +1305,21 @@ class SafeguardTag extends ArenaTag {
}
class NoneTag extends ArenaTag {
public readonly tagType = ArenaTagType.NONE;
constructor() {
super(ArenaTagType.NONE, 0);
super(0);
}
}
/**
* This arena tag facilitates the application of the move Imprison
* Imprison remains in effect as long as the source Pokemon is active and present on the field.
* Imprison will apply to any opposing Pokemon that switch onto the field as well.
*/
class ImprisonTag extends ArenaTrapTag {
constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.IMPRISON, MoveId.IMPRISON, sourceId, side, 1);
public readonly tagType = ArenaTagType.IMPRISON;
constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(MoveId.IMPRISON, sourceId, side, 1);
}
/**
@ -1255,7 +1374,9 @@ class ImprisonTag extends ArenaTrapTag {
*/
override onRemove(): void {
const party = this.getAffectedPokemon();
party.forEach(p => p.removeTag(BattlerTagType.IMPRISON));
party.forEach(p => {
p.removeTag(BattlerTagType.IMPRISON);
});
}
}
@ -1266,9 +1387,10 @@ class ImprisonTag extends ArenaTrapTag {
* Damages all non-Fire-type Pokemon on the given side of the field at the end
* of each turn for 4 turns.
*/
class FireGrassPledgeTag extends ArenaTag {
constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.FIRE_GRASS_PLEDGE, 4, MoveId.FIRE_PLEDGE, sourceId, side);
class FireGrassPledgeTag extends SerializableArenaTag {
public readonly tagType = ArenaTagType.FIRE_GRASS_PLEDGE;
constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(4, MoveId.FIRE_PLEDGE, sourceId, side);
}
override onAdd(_arena: Arena): void {
@ -1314,9 +1436,10 @@ class FireGrassPledgeTag extends ArenaTag {
* Doubles the secondary effect chance of moves from Pokemon on the
* given side of the field for 4 turns.
*/
class WaterFirePledgeTag extends ArenaTag {
constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.WATER_FIRE_PLEDGE, 4, MoveId.WATER_PLEDGE, sourceId, side);
class WaterFirePledgeTag extends SerializableArenaTag {
public readonly tagType = ArenaTagType.WATER_FIRE_PLEDGE;
constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(4, MoveId.WATER_PLEDGE, sourceId, side);
}
override onAdd(_arena: Arena): void {
@ -1348,9 +1471,10 @@ class WaterFirePledgeTag extends ArenaTag {
* and {@link https://bulbapedia.bulbagarden.net/wiki/Water_Pledge_(move) | Water Pledge}.
* Quarters the Speed of Pokemon on the given side of the field for 4 turns.
*/
class GrassWaterPledgeTag extends ArenaTag {
constructor(sourceId: number, side: ArenaTagSide) {
super(ArenaTagType.GRASS_WATER_PLEDGE, 4, MoveId.GRASS_PLEDGE, sourceId, side);
class GrassWaterPledgeTag extends SerializableArenaTag {
public readonly tagType = ArenaTagType.GRASS_WATER_PLEDGE;
constructor(sourceId: number | undefined, side: ArenaTagSide) {
super(4, MoveId.GRASS_PLEDGE, sourceId, side);
}
override onAdd(_arena: Arena): void {
@ -1370,9 +1494,10 @@ class GrassWaterPledgeTag extends ArenaTag {
* If a Pokémon that's on the field when Fairy Lock is used goes on to faint later in the same turn,
* the Pokémon that replaces it will still be unable to switch out in the following turn.
*/
export class FairyLockTag extends ArenaTag {
constructor(turnCount: number, sourceId: number) {
super(ArenaTagType.FAIRY_LOCK, turnCount, MoveId.FAIRY_LOCK, sourceId);
export class FairyLockTag extends SerializableArenaTag {
public readonly tagType = ArenaTagType.FAIRY_LOCK;
constructor(turnCount: number, sourceId?: number) {
super(turnCount, MoveId.FAIRY_LOCK, sourceId);
}
onAdd(_arena: Arena): void {
@ -1386,15 +1511,29 @@ export class FairyLockTag extends ArenaTag {
* Keeps track of the number of pokemon on the field with Neutralizing Gas - If it drops to zero, the effect is ended and abilities are reactivated
*
* Additionally ends onLose abilities when it is activated
* @sealed
*/
export class SuppressAbilitiesTag extends ArenaTag {
private sourceCount: number;
private beingRemoved: boolean;
export class SuppressAbilitiesTag extends SerializableArenaTag {
// Source count is allowed to be inwardly mutable, but outwardly immutable
public readonly sourceCount: number;
public readonly tagType = ArenaTagType.NEUTRALIZING_GAS;
// Private field prevents field from appearing during serialization
/** Whether the tag is in the process of being removed */
#beingRemoved: boolean;
/** Whether the tag is in the process of being removed */
public get beingRemoved(): boolean {
return this.#beingRemoved;
}
constructor(sourceId: number) {
super(ArenaTagType.NEUTRALIZING_GAS, 0, undefined, sourceId);
constructor(sourceId?: number) {
super(0, undefined, sourceId);
this.sourceCount = 1;
this.beingRemoved = false;
this.#beingRemoved = false;
}
public override loadTag(source: NonFunctionProperties<SuppressAbilitiesTag>): void {
super.loadTag(source);
(this as Mutable<this>).sourceCount = source.sourceCount;
}
public override onAdd(_arena: Arena): void {
@ -1406,19 +1545,21 @@ export class SuppressAbilitiesTag extends ArenaTag {
if (fieldPokemon && fieldPokemon.id !== pokemon.id) {
// TODO: investigate whether we can just remove the foreach and call `applyAbAttrs` directly, providing
// the appropriate attributes (preLEaveField and IllusionBreak)
[true, false].forEach(passive => applyOnLoseAbAttrs({ pokemon: fieldPokemon, passive }));
[true, false].forEach(passive => {
applyOnLoseAbAttrs({ pokemon: fieldPokemon, passive });
});
}
}
}
}
public override onOverlap(_arena: Arena, source: Pokemon | null): void {
this.sourceCount++;
(this as Mutable<this>).sourceCount++;
this.playActivationMessage(source);
}
public onSourceLeave(arena: Arena): void {
this.sourceCount--;
(this as Mutable<this>).sourceCount--;
if (this.sourceCount <= 0) {
arena.removeTag(ArenaTagType.NEUTRALIZING_GAS);
} else if (this.sourceCount === 1) {
@ -1436,7 +1577,7 @@ export class SuppressAbilitiesTag extends ArenaTag {
}
public override onRemove(_arena: Arena, quiet = false) {
this.beingRemoved = true;
this.#beingRemoved = true;
if (!quiet) {
globalScene.phaseManager.queueMessage(i18next.t("arenaTag:neutralizingGasOnRemove"));
}
@ -1444,7 +1585,9 @@ export class SuppressAbilitiesTag extends ArenaTag {
for (const pokemon of globalScene.getField(true)) {
// There is only one pokemon with this attr on the field on removal, so its abilities are already active
if (pokemon && !pokemon.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false)) {
[true, false].forEach(passive => applyOnGainAbAttrs({ pokemon, passive }));
[true, false].forEach(passive => {
applyOnGainAbAttrs({ pokemon, passive });
});
}
}
}
@ -1453,10 +1596,6 @@ export class SuppressAbilitiesTag extends ArenaTag {
return this.sourceCount > 1;
}
public isBeingRemoved() {
return this.beingRemoved;
}
private playActivationMessage(pokemon: Pokemon | null) {
if (pokemon) {
globalScene.phaseManager.queueMessage(
@ -1473,7 +1612,7 @@ export function getArenaTag(
tagType: ArenaTagType,
turnCount: number,
sourceMove: MoveId | undefined,
sourceId: number,
sourceId: number | undefined,
side: ArenaTagSide = ArenaTagSide.BOTH,
): ArenaTag | null {
switch (tagType) {
@ -1488,7 +1627,7 @@ export function getArenaTag(
case ArenaTagType.CRAFTY_SHIELD:
return new CraftyShieldTag(sourceId, side);
case ArenaTagType.NO_CRIT:
return new NoCritTag(turnCount, sourceMove!, sourceId, side); // TODO: is this bang correct?
return new NoCritTag(turnCount, sourceMove, sourceId, side);
case ArenaTagType.MUD_SPORT:
return new MudSportTag(turnCount, sourceId);
case ArenaTagType.WATER_SPORT:
@ -1506,7 +1645,7 @@ export function getArenaTag(
case ArenaTagType.TRICK_ROOM:
return new TrickRoomTag(turnCount, sourceId);
case ArenaTagType.GRAVITY:
return new GravityTag(turnCount);
return new GravityTag(turnCount, sourceId);
case ArenaTagType.REFLECT:
return new ReflectTag(turnCount, sourceId, side);
case ArenaTagType.LIGHT_SCREEN:
@ -1538,12 +1677,43 @@ export function getArenaTag(
/**
* When given a battler tag or json representing one, creates an actual ArenaTag object with the same data.
* @param {ArenaTag | any} source An arena tag
* @return {ArenaTag} The valid arena tag
* @param source - An arena tag
* @returns The valid arena tag
*/
export function loadArenaTag(source: ArenaTag | any): ArenaTag {
export function loadArenaTag(source: ArenaTag | ArenaTagTypeData): ArenaTag {
const tag =
getArenaTag(source.tagType, source.sourceId, source.sourceMove, source.turnCount, source.side) ?? new NoneTag();
tag.loadTag(source);
return tag;
}
export type ArenaTagTypeMap = {
[ArenaTagType.MUD_SPORT]: MudSportTag;
[ArenaTagType.WATER_SPORT]: WaterSportTag;
[ArenaTagType.ION_DELUGE]: IonDelugeTag;
[ArenaTagType.SPIKES]: SpikesTag;
[ArenaTagType.MIST]: MistTag;
[ArenaTagType.QUICK_GUARD]: QuickGuardTag;
[ArenaTagType.WIDE_GUARD]: WideGuardTag;
[ArenaTagType.MAT_BLOCK]: MatBlockTag;
[ArenaTagType.CRAFTY_SHIELD]: CraftyShieldTag;
[ArenaTagType.NO_CRIT]: NoCritTag;
[ArenaTagType.TOXIC_SPIKES]: ToxicSpikesTag;
[ArenaTagType.STEALTH_ROCK]: StealthRockTag;
[ArenaTagType.STICKY_WEB]: StickyWebTag;
[ArenaTagType.TRICK_ROOM]: TrickRoomTag;
[ArenaTagType.GRAVITY]: GravityTag;
[ArenaTagType.REFLECT]: ReflectTag;
[ArenaTagType.LIGHT_SCREEN]: LightScreenTag;
[ArenaTagType.AURORA_VEIL]: AuroraVeilTag;
[ArenaTagType.TAILWIND]: TailwindTag;
[ArenaTagType.HAPPY_HOUR]: HappyHourTag;
[ArenaTagType.SAFEGUARD]: SafeguardTag;
[ArenaTagType.IMPRISON]: ImprisonTag;
[ArenaTagType.FIRE_GRASS_PLEDGE]: FireGrassPledgeTag;
[ArenaTagType.WATER_FIRE_PLEDGE]: WaterFirePledgeTag;
[ArenaTagType.GRASS_WATER_PLEDGE]: GrassWaterPledgeTag;
[ArenaTagType.FAIRY_LOCK]: FairyLockTag;
[ArenaTagType.NEUTRALIZING_GAS]: SuppressAbilitiesTag;
[ArenaTagType.NONE]: NoneTag;
};

View File

@ -74,10 +74,13 @@ export class BattlerTag {
/**
* Tick down this {@linkcode BattlerTag}'s duration.
* @returns `true` if the tag should be kept (`turnCount > 0`)
* @param _pokemon - The {@linkcode Pokemon} whom this tag belongs to.
* Unused by default but can be used by subclasses.
* @param _lapseType - The {@linkcode BattlerTagLapseType} being lapsed.
* Unused by default but can be used by subclasses.
* @returns `true` if the tag should be kept (`turnCount` > 0`)
*/
lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean {
// TODO: Maybe flip this (return `true` if tag needs removal)
return --this.turnCount > 0;
}
@ -123,7 +126,7 @@ export interface TerrainBattlerTag {
/**
* Base class for tags that restrict the usage of moves. This effect is generally referred to as "disabling" a move
* in-game. This is not to be confused with {@linkcode MoveId.DISABLE}.
* in-game (not to be confused with {@linkcode MoveId.DISABLE}).
*
* Descendants can override {@linkcode isMoveRestricted} to restrict moves that
* match a condition. A restricted move gets cancelled before it is used.

View File

@ -168,9 +168,9 @@ export abstract class Move implements Localizable {
}
/**
* Get all move attributes that match `attrType`
* @param attrType any attribute that extends {@linkcode MoveAttr}
* @returns Array of attributes that match `attrType`, Empty Array if none match.
* Get all move attributes that match `attrType`.
* @param attrType - The name of a {@linkcode MoveAttr} to search for
* @returns An array containing all attributes matching `attrType`, or an empty array if none match.
*/
getAttrs<T extends MoveAttrString>(attrType: T): (MoveAttrMap[T])[] {
const targetAttr = MoveAttrs[attrType];
@ -181,9 +181,9 @@ export abstract class Move implements Localizable {
}
/**
* Check if a move has an attribute that matches `attrType`
* @param attrType any attribute that extends {@linkcode MoveAttr}
* @returns true if the move has attribute `attrType`
* Check if a move has an attribute that matches `attrType`.
* @param attrType - The name of a {@linkcode MoveAttr} to search for
* @returns Whether this move has at least 1 attribute that matches `attrType`
*/
hasAttr(attrType: MoveAttrString): boolean {
const targetAttr = MoveAttrs[attrType];
@ -195,23 +195,25 @@ export abstract class Move implements Localizable {
}
/**
* Takes as input a boolean function and returns the first MoveAttr in attrs that matches true
* @param attrPredicate
* @returns the first {@linkcode MoveAttr} element in attrs that makes the input function return true
* Find the first attribute that matches a given predicate function.
* @param attrPredicate - The predicate function to search `MoveAttr`s by
* @returns The first {@linkcode MoveAttr} for which `attrPredicate` returns `true`
*/
findAttr(attrPredicate: (attr: MoveAttr) => boolean): MoveAttr {
return this.attrs.find(attrPredicate)!; // TODO: is the bang correct?
// TODO: Remove bang and make return type `MoveAttr | undefined`,
// as well as add overload for functions of type `x is T`
return this.attrs.find(attrPredicate)!;
}
/**
* Adds a new MoveAttr to the move (appends to the attr array)
* if the MoveAttr also comes with a condition, also adds that to the conditions array: {@linkcode MoveCondition}
* @param AttrType {@linkcode MoveAttr} the constructor of a MoveAttr class
* @param args the args needed to instantiate a the given class
* @returns the called object {@linkcode Move}
* Adds a new MoveAttr to this move (appends to the attr array).
* If the MoveAttr also comes with a condition, it is added to its {@linkcode MoveCondition} array.
* @param attrType - The {@linkcode MoveAttr} to add
* @param args - The arguments needed to instantiate the given class
* @returns `this`
*/
attr<T extends Constructor<MoveAttr>>(AttrType: T, ...args: ConstructorParameters<T>): this {
const attr = new AttrType(...args);
attr<T extends Constructor<MoveAttr>>(attrType: T, ...args: ConstructorParameters<T>): this {
const attr = new attrType(...args);
this.attrs.push(attr);
let attrCondition = attr.getCondition();
if (attrCondition) {
@ -225,11 +227,13 @@ export abstract class Move implements Localizable {
}
/**
* Adds a new MoveAttr to the move (appends to the attr array)
* if the MoveAttr also comes with a condition, also adds that to the conditions array: {@linkcode MoveCondition}
* Almost identical to {@link attr}, except you are passing in a MoveAttr object, instead of a constructor and it's arguments
* @param attrAdd {@linkcode MoveAttr} the attribute to add
* @returns the called object {@linkcode Move}
* Adds a new MoveAttr to this move (appends to the attr array).
* If the MoveAttr also comes with a condition, it is added to its {@linkcode MoveCondition} array.
*
* Similar to {@linkcode attr}, except this takes an already instantiated {@linkcode MoveAttr} object
* as opposed to a constructor and its arguments.
* @param attrAdd - The {@linkcode MoveAttr} to add
* @returns `this`
*/
addAttr(attrAdd: MoveAttr): this {
this.attrs.push(attrAdd);
@ -246,8 +250,8 @@ export abstract class Move implements Localizable {
/**
* Sets the move target of this move
* @param moveTarget {@linkcode MoveTarget} the move target to set
* @returns the called object {@linkcode Move}
* @param moveTarget - The {@linkcode MoveTarget} to set
* @returns `this`
*/
target(moveTarget: MoveTarget): this {
this.moveTarget = moveTarget;
@ -255,13 +259,13 @@ export abstract class Move implements Localizable {
}
/**
* Getter function that returns if this Move has a MoveFlag
* @param flag {@linkcode MoveFlags} to check
* @returns boolean
* Getter function that returns if this Move has a given MoveFlag.
* @param flag - The {@linkcode MoveFlags} to check
* @returns Whether this Move has the specified flag.
*/
hasFlag(flag: MoveFlags): boolean {
// internally it is taking the bitwise AND (MoveFlags are represented as bit-shifts) and returning False if result is 0 and true otherwise
return !!(this.flags & flag);
// Flags are internally represented as bitmasks, so we check by taking the bitwise AND.
return (this.flags & flag) !== MoveFlags.NONE;
}
/**
@ -306,13 +310,13 @@ export abstract class Move implements Localizable {
}
/**
* Checks if the move is immune to certain types.
*
* Checks if the target is immune to this Move's type.
* Currently looks at cases of Grass types with powder moves and Dark types with moves affected by Prankster.
* @param user - The source of this move
* @param target - The target of this move
* @param type - The type of the move's target
* @returns boolean
* @param user - The {@linkcode Pokemon} using this move
* @param target - The {@linkcode Pokemon} targeted by this move
* @param type - The {@linkcode PokemonType} of the target
* @returns Whether the move is blocked by the target's type.
* Self-targeted moves will return `false` regardless of circumstances.
*/
isTypeImmune(user: Pokemon, target: Pokemon, type: PokemonType): boolean {
if (this.moveTarget === MoveTarget.USER) {
@ -326,7 +330,7 @@ export abstract class Move implements Localizable {
}
break;
case PokemonType.DARK:
if (user.hasAbility(AbilityId.PRANKSTER) && this.category === MoveCategory.STATUS && (user.isPlayer() !== target.isPlayer())) {
if (user.hasAbility(AbilityId.PRANKSTER) && this.category === MoveCategory.STATUS && user.isOpponent(target)) {
return true;
}
break;
@ -336,9 +340,9 @@ export abstract class Move implements Localizable {
/**
* Checks if the move would hit its target's Substitute instead of the target itself.
* @param user The {@linkcode Pokemon} using this move
* @param target The {@linkcode Pokemon} targeted by this move
* @returns `true` if the move can bypass the target's Substitute; `false` otherwise.
* @param user - The {@linkcode Pokemon} using this move
* @param target - The {@linkcode Pokemon} targeted by this move
* @returns Whether this Move will hit the target's Substitute (assuming one exists).
*/
hitsSubstitute(user: Pokemon, target?: Pokemon): boolean {
if ([ MoveTarget.USER, MoveTarget.USER_SIDE, MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES ].includes(this.moveTarget)
@ -356,13 +360,14 @@ export abstract class Move implements Localizable {
}
/**
* Adds a move condition to the move
* @param condition {@linkcode MoveCondition} or {@linkcode MoveConditionFunc}, appends to conditions array a new MoveCondition object
* @returns the called object {@linkcode Move}
* Adds a condition to this move (in addition to any provided by its prior {@linkcode MoveAttr}s).
* The move will fail upon use if at least 1 of its conditions is not met.
* @param condition - The {@linkcode MoveCondition} or {@linkcode MoveConditionFunc} to add to the conditions array.
* @returns `this`
*/
condition(condition: MoveCondition | MoveConditionFunc): this {
if (typeof condition === "function") {
condition = new MoveCondition(condition as MoveConditionFunc);
condition = new MoveCondition(condition);
}
this.conditions.push(condition);
@ -370,16 +375,22 @@ export abstract class Move implements Localizable {
}
/**
* Internal dev flag for documenting edge cases. When using this, please document the known edge case.
* @returns the called object {@linkcode Move}
* Mark a move as having one or more edge cases.
* The move may lack certain niche interactions with other moves/abilities,
* but still functions as intended in most cases.
*
* When using this, **make sure to document the edge case** (or else this becomes pointless).
* @returns `this`
*/
edgeCase(): this {
return this;
}
/**
* Marks the move as "partial": appends texts to the move name
* @returns the called object {@linkcode Move}
* Mark this move as partially implemented.
* Partial moves are expected to have some core functionality implemented, but may lack
* certain notable features or interactions with other moves or abilities.
* @returns `this`
*/
partial(): this {
this.nameAppend += " (P)";
@ -387,8 +398,10 @@ export abstract class Move implements Localizable {
}
/**
* Marks the move as "unimplemented": appends texts to the move name
* @returns the called object {@linkcode Move}
* Mark this move as unimplemented.
* Unimplemented moves are ones which have _none_ of their basic functionality enabled,
* and cannot be used.
* @returns `this`
*/
unimplemented(): this {
this.nameAppend += " (N)";
@ -963,10 +976,8 @@ export class AttackMove extends Move {
constructor(id: MoveId, type: PokemonType, category: MoveCategory, power: number, accuracy: number, pp: number, chance: number, priority: number, generation: number) {
super(id, type, category, MoveTarget.NEAR_OTHER, power, accuracy, pp, chance, priority, generation);
/**
* {@link https://bulbapedia.bulbagarden.net/wiki/Freeze_(status_condition)}
* > All damaging Fire-type moves can now thaw a frozen target, regardless of whether or not they have a chance to burn;
*/
// > All damaging Fire-type moves can... thaw a frozen target, regardless of whether or not they have a chance to burn.
// - https://bulbapedia.bulbagarden.net/wiki/Freeze_(status_condition)
if (this.type === PokemonType.FIRE) {
this.addAttr(new HealStatusEffectAttr(false, StatusEffect.FREEZE));
}
@ -1222,7 +1233,8 @@ interface MoveEffectAttrOptions {
effectChanceOverride?: number;
}
/** Base class defining all Move Effect Attributes
/**
* Base class defining all Move Effect Attributes
* @extends MoveAttr
* @see {@linkcode apply}
*/
@ -1240,8 +1252,7 @@ export class MoveEffectAttr extends MoveAttr {
/**
* Defines when this effect should trigger in the move's effect order.
* @default MoveEffectTrigger.POST_APPLY
* @see {@linkcode MoveEffectTrigger}
* @defaultValue {@linkcode MoveEffectTrigger.POST_APPLY}
*/
public get trigger () {
return this.options?.trigger ?? MoveEffectTrigger.POST_APPLY;
@ -1250,7 +1261,7 @@ export class MoveEffectAttr extends MoveAttr {
/**
* `true` if this effect should only trigger on the first hit of
* multi-hit moves.
* @default false
* @defaultValue `false`
*/
public get firstHitOnly () {
return this.options?.firstHitOnly ?? false;
@ -1259,7 +1270,7 @@ export class MoveEffectAttr extends MoveAttr {
/**
* `true` if this effect should only trigger on the last hit of
* multi-hit moves.
* @default false
* @defaultValue `false`
*/
public get lastHitOnly () {
return this.options?.lastHitOnly ?? false;
@ -1268,7 +1279,7 @@ export class MoveEffectAttr extends MoveAttr {
/**
* `true` if this effect should apply only upon hitting a target
* for the first time when targeting multiple {@linkcode Pokemon}.
* @default false
* @defaultValue `false`
*/
public get firstTargetOnly () {
return this.options?.firstTargetOnly ?? false;
@ -2572,9 +2583,12 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
}
/**
* Applies the effect of Psycho Shift to its target
* Psycho Shift takes the user's status effect and passes it onto the target. The user is then healed after the move has been successfully executed.
* @returns `true` if Psycho Shift's effect is able to be applied to the target
* Applies the effect of {@linkcode Moves.PSYCHO_SHIFT} to its target.
* Psycho Shift takes the user's status effect and passes it onto the target.
* The user is then healed after the move has been successfully executed.
* @param user - The {@linkcode Pokemon} using the move
* @param target - The {@linkcode Pokemon} targeted by the move.
* @returns - Whether the effect was successfully applied to the target.
*/
apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
@ -2907,6 +2921,12 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
}
}
/**
* Attribute to add the {@linkcode BattlerTagType.BYPASS_SLEEP | BYPASS_SLEEP Battler Tag} for 1 turn to the user before move use.
* Used by {@linkcode Moves.SNORE} and {@linkcode Moves.SLEEP_TALK}.
*/
// TODO: Should this use a battler tag?
// TODO: Give this `userSleptOrComatoseCondition` by default
export class BypassSleepAttr extends MoveAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (user.status?.effect === StatusEffect.SLEEP) {
@ -2924,7 +2944,7 @@ export class BypassSleepAttr extends MoveAttr {
* @param move
*/
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
return user.status && user.status.effect === StatusEffect.SLEEP ? 200 : -10;
return user.status?.effect === StatusEffect.SLEEP ? 200 : -10;
}
}
@ -3309,7 +3329,7 @@ export class StatStageChangeAttr extends MoveEffectAttr {
/**
* `true` to display a message for the stat change.
* @default true
* @defaultValue `true`
*/
private get showMessage () {
return this.options?.showMessage ?? true;
@ -5472,7 +5492,10 @@ export class NoEffectAttr extends MoveAttr {
}
}
const crashDamageFunc = (user: Pokemon, move: Move) => {
/**
* Function to deal Crash Damage (1/2 max hp) to the user on apply.
*/
const crashDamageFunc: UserMoveConditionFunc = (user: Pokemon, move: Move) => {
const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: user, cancelled});
if (cancelled.value) {
@ -7111,7 +7134,8 @@ export class CopyMoveAttr extends CallMoveAttr {
/**
* Attribute used for moves that cause the target to repeat their last used move.
*
* Used for [Instruct](https://bulbapedia.bulbagarden.net/wiki/Instruct_(move)).
* Used by {@linkcode Moves.INSTRUCT | Instruct}.
* @see [Instruct on Bulbapedia](https://bulbapedia.bulbagarden.net/wiki/Instruct_(move))
*/
export class RepeatMoveAttr extends MoveEffectAttr {
private movesetMove: PokemonMove;

View File

@ -36,13 +36,13 @@ export class PokemonMove {
}
/**
* Checks whether the move can be selected or performed by a Pokemon, without consideration for the move's targets.
* Checks whether this move can be selected/performed by a Pokemon, without consideration for the move's targets.
* The move is unusable if it is out of PP, restricted by an effect, or unimplemented.
*
* @param pokemon - {@linkcode Pokemon} that would be using this move
* @param ignorePp - If `true`, skips the PP check
* @param ignoreRestrictionTags - If `true`, skips the check for move restriction tags (see {@link MoveRestrictionBattlerTag})
* @returns `true` if the move can be selected and used by the Pokemon, otherwise `false`.
* @param pokemon - The {@linkcode Pokemon} attempting to use this move
* @param ignorePp - Whether to ignore checking if the move is out of PP; default `false`
* @param ignoreRestrictionTags - Whether to skip checks for {@linkcode MoveRestrictionBattlerTag}s; default `false`
* @returns Whether this {@linkcode PokemonMove} can be selected by this Pokemon.
*/
isUsable(pokemon: Pokemon, ignorePp = false, ignoreRestrictionTags = false): boolean {
// TODO: Add Sky Drop's 1 turn stall

View File

@ -406,12 +406,12 @@ export async function applyModifierTypeToPlayerPokemon(
// Check if the Pokemon has max stacks of that item already
const modifier = modType.newModifier(pokemon);
const existing = globalScene.findModifier(
m =>
(m): m is PokemonHeldItemModifier =>
m instanceof PokemonHeldItemModifier &&
m.type.id === modType.id &&
m.pokemonId === pokemon.id &&
m.matchType(modifier),
) as PokemonHeldItemModifier;
) as PokemonHeldItemModifier | undefined;
// At max stacks
if (existing && existing.getStackCount() >= existing.getMaxStackCount()) {

View File

@ -13,6 +13,11 @@ export enum TerrainType {
PSYCHIC,
}
export interface SerializedTerrain {
terrainType: TerrainType;
turnsLeft: number;
}
export class Terrain {
public terrainType: TerrainType;
public turnsLeft: number;

View File

@ -11,6 +11,11 @@ import type { Move } from "#moves/move";
import { randSeedInt } from "#utils/common";
import i18next from "i18next";
export interface SerializedWeather {
weatherType: WeatherType;
turnsLeft: number;
}
export class Weather {
public weatherType: WeatherType;
public turnsLeft: number;

View File

@ -1,3 +1,7 @@
/**
* A list of possible flags that various moves may have.
* Represented internally as a bitmask.
*/
export enum MoveFlags {
NONE = 0,
MAKES_CONTACT = 1 << 0,

View File

@ -1,3 +1,13 @@
import type { ArenaTagTypeMap } from "#data/arena-tag";
import type { NonSerializableArenaTagType, SerializableArenaTagType } from "#types/arena-tags";
/**
* Enum representing all different types of {@linkcode ArenaTag}s.
* @privateRemarks
* When modifying the fields in this enum, ensure that:
* - The entry is added to / removed from {@linkcode ArenaTagTypeMap}
* - The tag is added to / removed from {@linkcode NonSerializableArenaTagType} or {@linkcode SerializableArenaTagType}
*/
export enum ArenaTagType {
NONE = "NONE",
MUD_SPORT = "MUD_SPORT",

View File

@ -1,9 +1,7 @@
/**
* Determines the selected battle style.
* - 'Switch' - The option to switch the active pokemon at the start of a battle will be displayed.
* - 'Set' - The option to switch the active pokemon at the start of a battle will not display.
*/
/** Enum for selected battle style. */
export enum BattleStyle {
SWITCH,
SET
/** Display option to switch active pokemon at battle start. */
SWITCH,
/** Hide option to switch active pokemon at battle start. */
SET
}

View File

@ -1,3 +1,7 @@
/**
* The index of a given Pokemon on-field.
* Used as an index into `globalScene.getField`, as well as for most target-specifying effects.
*/
export enum BattlerIndex {
ATTACKER = -1,
PLAYER,

View File

@ -1,15 +1,4 @@
/**
* Defines the speed of gaining experience.
*
* @remarks
* The `expGainSpeed` can have several modes:
* - `0` - Default: The normal speed.
* - `1` - Fast: Fast speed.
* - `2` - Faster: Faster speed.
* - `3` - Skip: Skip gaining exp animation.
*
* @default 0 - Uses the default normal speed.
*/
/** Defines the speed of gaining experience. */
export enum ExpGainsSpeed {
/** The normal speed. */
DEFAULT,

View File

@ -1,11 +1,9 @@
/**
* Determines exp notification style.
* - Default - the normal exp gain display, nothing changed
* - Only level up - we display the level up in the small frame instead of a message
* - Skip - no level up frame nor message
*/
/** Enum for party experience gain notification style. */
export enum ExpNotification {
DEFAULT,
ONLY_LEVEL_UP,
SKIP
/** Display amount flyout for all off-field party members upon gaining any amount of EXP. */
DEFAULT,
/** Display smaller flyout showing level gained on gaining a new level. */
ONLY_LEVEL_UP,
/** Do not show any flyouts for EXP gains or levelups. */
SKIP
}

View File

@ -33,6 +33,7 @@ import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEven
import type { Pokemon } from "#field/pokemon";
import { FieldEffectModifier } from "#modifiers/modifier";
import type { Move } from "#moves/move";
import type { AbstractConstructor } from "#types/type-helpers";
import { type Constructor, isNullOrUndefined, NumberHolder, randSeedInt } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
@ -652,7 +653,7 @@ export class Arena {
* @param args array of parameters that the called upon tags may need
*/
applyTagsForSide(
tagType: ArenaTagType | Constructor<ArenaTag>,
tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>,
side: ArenaTagSide,
simulated: boolean,
...args: unknown[]
@ -674,7 +675,11 @@ export class Arena {
* @param simulated if `true`, this applies arena tags without changing game state
* @param args array of parameters that the called upon tags may need
*/
applyTags(tagType: ArenaTagType | Constructor<ArenaTag>, simulated: boolean, ...args: unknown[]): void {
applyTags(
tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>,
simulated: boolean,
...args: unknown[]
): void {
this.applyTagsForSide(tagType, ArenaTagSide.BOTH, simulated, ...args);
}
@ -727,21 +732,19 @@ export class Arena {
/**
* Attempt to get a tag from the Arena via {@linkcode getTagOnSide} that applies to both sides
* @param tagType The {@linkcode ArenaTagType} to retrieve
* @param tagType - The {@linkcode ArenaTagType} to retrieve
* @returns The existing {@linkcode ArenaTag}, or `undefined` if not present.
* @overload
*/
getTag(tagType: ArenaTagType): ArenaTag | undefined;
/**
* Attempt to get a tag from the Arena via {@linkcode getTagOnSide} that applies to both sides
* @param tagType The {@linkcode ArenaTag} to retrieve
* @param tagType - The constructor of the {@linkcode ArenaTag} to retrieve
* @returns The existing {@linkcode ArenaTag}, or `undefined` if not present.
* @overload
*/
getTag<T extends ArenaTag>(tagType: Constructor<T>): T | undefined;
getTag(tagType: ArenaTagType | Constructor<ArenaTag>): ArenaTag | undefined {
getTag<T extends ArenaTag>(tagType: Constructor<T> | AbstractConstructor<T>): T | undefined;
getTag(tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>): ArenaTag | undefined {
return this.getTagOnSide(tagType, ArenaTagSide.BOTH);
}
@ -757,7 +760,10 @@ export class Arena {
* @param side The {@linkcode ArenaTagSide} to look at
* @returns either the {@linkcode ArenaTag}, or `undefined` if it isn't there
*/
getTagOnSide(tagType: ArenaTagType | Constructor<ArenaTag>, side: ArenaTagSide): ArenaTag | undefined {
getTagOnSide(
tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>,
side: ArenaTagSide,
): ArenaTag | undefined {
return typeof tagType === "string"
? this.tags.find(
t => t.tagType === tagType && (side === ArenaTagSide.BOTH || t.side === ArenaTagSide.BOTH || t.side === side),

View File

@ -375,7 +375,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
this.ivs = ivs || getIvsFromId(this.id);
if (this.gender === undefined) {
this.generateGender();
this.gender = this.species.generateGender();
}
if (this.formIndex === undefined) {
@ -437,7 +437,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* @param useIllusion - Whether we want the fake name or the real name of the Pokemon (for Illusion ability).
* Return the name that will be displayed when this Pokemon is sent out into battle.
* @param useIllusion - Whether to consider this Pokemon's illusion if present; default `true`
* @returns The name to render for this {@linkcode Pokemon}.
*/
getNameToRender(useIllusion = true) {
const name: string =
@ -446,7 +448,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
!useIllusion && this.summonData.illusion ? this.summonData.illusion.basePokemon.nickname : this.nickname;
try {
if (nickname) {
return decodeURIComponent(escape(atob(nickname)));
return decodeURIComponent(escape(atob(nickname))); // TODO: Remove `atob` and `escape`... eventually...
}
return name;
} catch (err) {
@ -455,11 +457,13 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
getPokeball(useIllusion = false) {
if (useIllusion) {
return this.summonData.illusion?.pokeball ?? this.pokeball;
}
return this.pokeball;
/**
* Return this Pokemon's {@linkcode PokeballType}.
* @param useIllusion - Whether to consider this Pokemon's illusion if present; default `false`
* @returns The {@linkcode PokeballType} that will be shown when this Pokemon is sent out into battle.
*/
getPokeball(useIllusion = false): PokeballType {
return useIllusion && this.summonData.illusion ? this.summonData.illusion.pokeball : this.pokeball;
}
init(): void {
@ -516,17 +520,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Checks if a pokemon is fainted (ie: its `hp <= 0`).
* It's usually better to call {@linkcode isAllowedInBattle()}
* @param checkStatus `true` to also check that the pokemon's status is {@linkcode StatusEffect.FAINT}
* @returns `true` if the pokemon is fainted
* Usually should not be called directly in favor of calling {@linkcode isAllowedInBattle()}.
* @param checkStatus - Whether to also check that the pokemon's status is {@linkcode StatusEffect.FAINT}; default `false`
* @returns Whether this Pokemon is fainted, as described above.
*/
public isFainted(checkStatus = false): boolean {
return this.hp <= 0 && (!checkStatus || this.status?.effect === StatusEffect.FAINT);
}
/**
* Check if this pokemon is both not fainted and allowed to be in battle based on currently active challenges.
* @returns {boolean} `true` if pokemon is allowed in battle
* Check if this pokemon is both not fainted and allowed to be used based on currently active challenges.
* @returns Whether this Pokemon is allowed to partake in battle.
*/
public isAllowedInBattle(): boolean {
return !this.isFainted() && this.isAllowedInChallenge();
@ -534,8 +538,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Check if this pokemon is allowed based on any active challenges.
* It's usually better to call {@linkcode isAllowedInBattle()}
* @returns {boolean} `true` if pokemon is allowed in battle
* Usually should not be called directly in favor of consulting {@linkcode isAllowedInBattle()}.
* @returns Whether this Pokemon is allowed under the current challenge conditions.
*/
public isAllowedInChallenge(): boolean {
const challengeAllowed = new BooleanHolder(true);
@ -545,8 +549,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Checks if this {@linkcode Pokemon} is allowed in battle (ie: not fainted, and allowed under any active challenges).
* @param onField `true` to also check if the pokemon is currently on the field; default `false`
* @returns `true` if the pokemon is "active", as described above.
* @param onField - Whether to also check if the pokemon is currently on the field; default `false`
* @returns Whether this pokemon is considered "active", as described above.
* Returns `false` if there is no active {@linkcode BattleScene} or the pokemon is disallowed.
*/
public isActive(onField = false): boolean {
@ -703,7 +707,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
abstract getBattlerIndex(): BattlerIndex;
/**
* @param useIllusion - Whether we want the illusion or not.
* Load all assets needed for this Pokemon's use in battle
* @param ignoreOverride - Whether to ignore overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `true`
* @param useIllusion - Whether to consider this pokemon's active illusion; default `false`
* @returns A promise that resolves once all the corresponding assets have been loaded.
*/
async loadAssets(ignoreOverride = true, useIllusion = false): Promise<void> {
/** Promises that are loading assets and can be run concurrently. */
@ -832,11 +839,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Attempt to process variant sprite.
*
* @param cacheKey the cache key for the variant color sprite
* @param useExpSprite should the experimental sprite be used
* @param battleSpritePath the filename of the sprite
* Attempt to process variant sprite color caches.
* @param cacheKey - the cache key for the variant color sprite
* @param useExpSprite - Whether experimental sprites should be used if present
* @param battleSpritePath - the filename of the sprite
*/
async populateVariantColorCache(cacheKey: string, useExpSprite: boolean, battleSpritePath: string) {
const spritePath = `./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`;
@ -883,8 +889,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
return this.fusionSpecies.forms[this.fusionFormIndex].formKey;
}
getSpriteAtlasPath(ignoreOverride?: boolean): string {
// TODO: Add more documentation for all these attributes.
// They may be all similar, but what each one actually _does_ is quite unclear at first glance
getSpriteAtlasPath(ignoreOverride = false): string {
const spriteId = this.getSpriteId(ignoreOverride).replace(/_{2}/g, "/");
return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`;
}
@ -1027,10 +1037,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Get this {@linkcode Pokemon}'s {@linkcode PokemonSpeciesForm}.
* @param ignoreOverride - Whether to ignore overridden species from {@linkcode MoveId.TRANSFORM}, default `false`.
* This overrides `useIllusion` if `true`.
* @param useIllusion - `true` to use the speciesForm of the illusion; default `false`.
* Return this Pokemon's {@linkcode PokemonSpeciesForm | SpeciesForm}.
* @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* and overrides `useIllusion`.
* @param useIllusion - Whether to consider this Pokemon's illusion if present; default `false`.
* @returns This Pokemon's {@linkcode PokemonSpeciesForm}.
*/
getSpeciesForm(ignoreOverride = false, useIllusion = false): PokemonSpeciesForm {
if (!ignoreOverride && this.summonData.speciesForm) {
@ -1082,9 +1093,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* @param {boolean} useIllusion - Whether we want the fusionSpeciesForm of the illusion or not.
* Return the {@linkcode PokemonSpeciesForm | SpeciesForm} of this Pokemon's fusion counterpart.
* @param ignoreOverride - Whether to ignore species overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @param useIllusion - Whether to consider the species of this Pokemon's illusion; default `false`
* @returns The {@linkcode PokemonSpeciesForm} of this Pokemon's fusion counterpart.
*/
getFusionSpeciesForm(ignoreOverride?: boolean, useIllusion = false): PokemonSpeciesForm {
getFusionSpeciesForm(ignoreOverride = false, useIllusion = false): PokemonSpeciesForm {
const fusionSpecies: PokemonSpecies =
useIllusion && this.summonData.illusion ? this.summonData.illusion.fusionSpecies! : this.fusionSpecies!;
const fusionFormIndex =
@ -1406,17 +1420,19 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* Calculates and retrieves the final value of a stat considering any held
* items, move effects, opponent abilities, and whether there was a critical
* hit.
* @param stat the desired {@linkcode EffectiveStat}
* @param opponent the target {@linkcode Pokemon}
* @param move the {@linkcode Move} being used
* @param ignoreAbility determines whether this Pokemon's abilities should be ignored during the stat calculation
* @param ignoreOppAbility during an attack, determines whether the opposing Pokemon's abilities should be ignored during the stat calculation.
* @param ignoreAllyAbility during an attack, determines whether the ally Pokemon's abilities should be ignored during the stat calculation.
* @param isCritical determines whether a critical hit has occurred or not (`false` by default)
* @param simulated if `true`, nullifies any effects that produce any changes to game state from triggering
* @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false`
* @returns the final in-battle value of a stat
* @param stat - The desired {@linkcode EffectiveStat | Stat} to check.
* @param opponent - The {@linkcode Pokemon} being targeted, if applicable.
* @param move - The {@linkcode Move} being used, if any. Used to check ability ignoring effects and similar.
* @param ignoreAbility - Whether to ignore ability effects of the user; default `false`.
* @param ignoreOppAbility - Whether to ignore ability effects of the target; default `false`.
* @param ignoreAllyAbility - Whether to ignore ability effects of the user's allies; default `false`.
* @param isCritical - Whether a critical hit has occurred or not; default `false`.
* If `true`, will nullify offensive stat drops or defensive stat boosts.
* @param simulated - Whether to nullify any effects that produce changes to game state during calculations; default `true`
* @param ignoreHeldItems - Whether to ignore the user's held items during stat calculation; default `false`.
* @returns The final in-battle value for the given stat.
*/
// TODO: Replace the optional parameters with an object to make calling this method less cumbersome
getEffectiveStat(
stat: EffectiveStat,
opponent?: Pokemon,
@ -1436,6 +1452,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
// The Ruin abilities here are never ignored, but they reveal themselves on summon anyway
const fieldApplied = new BooleanHolder(false);
for (const pokemon of globalScene.getField(true)) {
// TODO: remove `canStack` toggle from ability as breaking out renders it useless
applyAbAttrs("FieldMultiplyStatAbAttr", {
pokemon,
stat,
@ -1448,6 +1465,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
break;
}
}
if (!ignoreAbility) {
applyAbAttrs("StatMultiplierAbAttr", {
pokemon: this,
@ -1647,9 +1665,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* @param useIllusion - Whether we want the fake or real gender (illusion ability).
* Return this Pokemon's {@linkcode Gender}.
* @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @param useIllusion - Whether to consider this pokemon's illusion if present; default `false`
* @returns the {@linkcode Gender} of this {@linkcode Pokemon}.
*/
getGender(ignoreOverride?: boolean, useIllusion = false): Gender {
getGender(ignoreOverride = false, useIllusion = false): Gender {
if (useIllusion && this.summonData.illusion) {
return this.summonData.illusion.gender;
}
@ -1660,9 +1681,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* @param useIllusion - Whether we want the fake or real gender (illusion ability).
* Return this Pokemon's fusion's {@linkcode Gender}.
* @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @param useIllusion - Whether to consider this pokemon's illusion if present; default `false`
* @returns The {@linkcode Gender} of this {@linkcode Pokemon}'s fusion.
*/
getFusionGender(ignoreOverride?: boolean, useIllusion = false): Gender {
getFusionGender(ignoreOverride = false, useIllusion = false): Gender {
if (useIllusion && this.summonData.illusion?.fusionGender) {
return this.summonData.illusion.fusionGender;
}
@ -1673,15 +1697,19 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* @param useIllusion - Whether we want the fake or real shininess (illusion ability).
* Check whether this Pokemon is shiny.
* @param useIllusion - Whether to consider this pokemon's illusion if present; default `false`
* @returns Whether this Pokemon is shiny
*/
isShiny(useIllusion = false): boolean {
if (!useIllusion && this.summonData.illusion) {
return !!(
return (
this.summonData.illusion.basePokemon?.shiny ||
(this.summonData.illusion.fusionSpecies && this.summonData.illusion.basePokemon?.fusionShiny)
(this.summonData.illusion.fusionSpecies && this.summonData.illusion.basePokemon?.fusionShiny) ||
false
);
}
return this.shiny || (this.isFusion(useIllusion) && this.fusionShiny);
}
@ -1700,9 +1728,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
*
* @param useIllusion - Whether we want the fake or real shininess (illusion ability).
* @returns `true` if the {@linkcode Pokemon} is shiny and the fusion is shiny as well, `false` otherwise
* Check whether this Pokemon is doubly shiny (both normal and fusion are shiny).
* @param useIllusion - Whether to consider this pokemon's illusion if present; default `false`
* @returns Whether this pokemon's base and fusion counterparts are both shiny.
*/
isDoubleShiny(useIllusion = false): boolean {
if (!useIllusion && this.summonData.illusion?.basePokemon) {
@ -1712,11 +1740,15 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
this.summonData.illusion.basePokemon.fusionShiny
);
}
return this.isFusion(useIllusion) && this.shiny && this.fusionShiny;
}
/**
* @param useIllusion - Whether we want the fake or real variant (illusion ability).
* Return this Pokemon's {@linkcode Variant | shiny variant}.
* Only meaningful if this pokemon is actually shiny.
* @param useIllusion - Whether to consider this pokemon's illusion if present; default `false`
* @returns The shiny variant of this Pokemon.
*/
getVariant(useIllusion = false): Variant {
if (!useIllusion && this.summonData.illusion) {
@ -1724,9 +1756,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
? this.summonData.illusion.basePokemon!.variant
: (Math.max(this.variant, this.fusionVariant) as Variant);
}
return !this.isFusion(true) ? this.variant : (Math.max(this.variant, this.fusionVariant) as Variant);
}
// TODO: Clarify how this differs from `getVariant`
getBaseVariant(doubleShiny: boolean): Variant {
if (doubleShiny) {
return this.summonData.illusion?.basePokemon?.variant ?? this.variant;
@ -1734,19 +1768,28 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
return this.getVariant();
}
/**
* Return this pokemon's overall luck value, based on its shininess (1 pt per variant lvl).
* @returns The luck value of this Pokemon.
*/
getLuck(): number {
return this.luck + (this.isFusion() ? this.fusionLuck : 0);
}
/**
* Return whether this {@linkcode Pokemon} is currently fused with anything.
* @param useIllusion - Whether to consider this pokemon's illusion if present; default `false`
* @returns Whether this Pokemon is currently fused with another species.
*/
isFusion(useIllusion = false): boolean {
if (useIllusion && this.summonData.illusion) {
return !!this.summonData.illusion.fusionSpecies;
}
return !!this.fusionSpecies;
return useIllusion && this.summonData.illusion ? !!this.summonData.illusion.fusionSpecies : !!this.fusionSpecies;
}
/**
* @param useIllusion - Whether we want the fake name or the real name of the Pokemon (for Illusion ability).
* Return this {@linkcode Pokemon}'s name.
* @param useIllusion - Whether to consider this pokemon's illusion if present; default `false`
* @returns This Pokemon's name.
* @see {@linkcode getNameToRender} - gets this Pokemon's display name.
*/
getName(useIllusion = false): string {
return !useIllusion && this.summonData.illusion?.basePokemon
@ -1755,19 +1798,19 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Checks if the {@linkcode Pokemon} has a fusion with the specified {@linkcode SpeciesId}.
* @param species the pokemon {@linkcode SpeciesId} to check
* @returns `true` if the {@linkcode Pokemon} has a fusion with the specified {@linkcode SpeciesId}, `false` otherwise
* Check whether this {@linkcode Pokemon} has a fusion with the specified {@linkcode SpeciesId}.
* @param species - The {@linkcode SpeciesId} to check against.
* @returns Whether this Pokemon is currently fused with the specified {@linkcode SpeciesId}.
*/
hasFusionSpecies(species: SpeciesId): boolean {
return this.fusionSpecies?.speciesId === species;
}
/**
* Checks if the {@linkcode Pokemon} has is the specified {@linkcode SpeciesId} or is fused with it.
* @param species the pokemon {@linkcode SpeciesId} to check
* @param formKey If provided, requires the species to be in that form
* @returns `true` if the pokemon is the species or is fused with it, `false` otherwise
* Check whether this {@linkcode Pokemon} either is or is fused with the given {@linkcode SpeciesId}.
* @param species - The {@linkcode SpeciesId} to check against.
* @param formKey - If provided, will require the species to be in the given form.
* @returns Whether this Pokemon has this species as either its base or fusion counterpart.
*/
hasSpecies(species: SpeciesId, formKey?: string): boolean {
if (isNullOrUndefined(formKey)) {
@ -1782,7 +1825,13 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
abstract isBoss(): boolean;
getMoveset(ignoreOverride?: boolean): PokemonMove[] {
/**
* Return all the {@linkcode PokemonMove}s that make up this Pokemon's moveset.
* Takes into account player/enemy moveset overrides (which will also override PP count).
* @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @returns An array of {@linkcode PokemonMove}, as described above.
*/
getMoveset(ignoreOverride = false): PokemonMove[] {
const ret = !ignoreOverride && this.summonData.moveset ? this.summonData.moveset : this.moveset;
// Overrides moveset based on arrays specified in overrides.ts
@ -1804,11 +1853,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Checks which egg moves have been unlocked for the {@linkcode Pokemon} based
* on the species it was met at or by the first {@linkcode Pokemon} in its evolution
* Check which egg moves have been unlocked for this {@linkcode Pokemon}.
* Looks at either the species it was met at or the first {@linkcode Species} in its evolution
* line that can act as a starter and provides those egg moves.
* @returns an array of {@linkcode MoveId}, the length of which is determined by how many
* egg moves are unlocked for that species.
* @returns An array of all {@linkcode MoveId}s that are egg moves and unlocked for this Pokemon.
*/
getUnlockedEggMoves(): MoveId[] {
const moves: MoveId[] = [];
@ -1825,13 +1873,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Gets all possible learnable level moves for the {@linkcode Pokemon},
* Get all possible learnable level moves for the {@linkcode Pokemon},
* excluding any moves already known.
*
* Available egg moves are only included if the {@linkcode Pokemon} was
* in the starting party of the run and if Fresh Start is not active.
* @returns an array of {@linkcode MoveId}, the length of which is determined
* by how many learnable moves there are for the {@linkcode Pokemon}.
* @returns An array of {@linkcode MoveId}s, as described above.
*/
public getLearnableLevelMoves(): MoveId[] {
let levelMoves = this.getLevelMoves(1, true, false, true).map(lm => lm[1]);
@ -1846,12 +1893,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Gets the types of a pokemon
* @param includeTeraType - `true` to include tera-formed type; Default: `false`
* @param forDefend - `true` if the pokemon is defending from an attack; Default: `false`
* @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false`
* @param useIllusion - `true` to return the types of the illusion instead of the actual types; Default: `false`
* @returns array of {@linkcode PokemonType}
* Evaluate and return this Pokemon's typing.
* @param includeTeraType - Whether to use this Pokemon's tera type if Terastallized; default `false`
* @param forDefend - Whether this Pokemon is currently receiving an attack; default `false`
* @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @param useIllusion - Whether to consider this Pokemon's illusion if present; default `false`
* @returns An array of {@linkcode PokemonType}s corresponding to this Pokemon's typing (real or percieved).
*/
public getTypes(
includeTeraType = false,
@ -1947,7 +1994,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
// remove UNKNOWN if other types are present
if (types.length > 1 && types.includes(PokemonType.UNKNOWN)) {
if (types.length > 1) {
const index = types.indexOf(PokemonType.UNKNOWN);
if (index !== -1) {
types.splice(index, 1);
@ -1968,24 +2015,25 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Checks if the pokemon's typing includes the specified type
* @param type - {@linkcode PokemonType} to check
* @param includeTeraType - `true` to include tera-formed type; Default: `true`
* @param forDefend - `true` if the pokemon is defending from an attack; Default: `false`
* @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false`
* @returns `true` if the Pokemon's type matches
* Check if this Pokemon's typing includes the specified type.
* @param type - The {@linkcode PokemonType} to check
* @param includeTeraType - Whether to use this Pokemon's tera type if Terastallized; default `true`
* @param forDefend - Whether this Pokemon is currently receiving an attack; default `false`
* @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @returns Whether this Pokemon is of the specified type.
*/
public isOfType(type: PokemonType, includeTeraType = true, forDefend = false, ignoreOverride = false): boolean {
return this.getTypes(includeTeraType, forDefend, ignoreOverride).some(t => t === type);
return this.getTypes(includeTeraType, forDefend, ignoreOverride).includes(type);
}
/**
* Gets the non-passive ability of the pokemon. This accounts for fusions and ability changing effects.
* This should rarely be called, most of the time {@linkcode hasAbility} or {@linkcode hasAbilityWithAttr} are better used as
* those check both the passive and non-passive abilities and account for ability suppression.
* @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases
* @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false`
* @returns The non-passive {@linkcode Ability} of the pokemon
* Get this Pokemon's non-passive {@linkcode Ability}, factoring in fusions, overrides and ability-changing effects.
* Should rarely be called directly in favor of {@linkcode hasAbility} or {@linkcode hasAbilityWithAttr},
* both of which check both ability slots and account for suppression.
* @see {@linkcode hasAbility} and {@linkcode hasAbilityWithAttr} are the intended ways to check abilities in most cases
* @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @returns The non-passive {@linkcode Ability} of this Pokemon.
*/
public getAbility(ignoreOverride = false): Ability {
if (!ignoreOverride && this.summonData.ability) {
@ -2131,7 +2179,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (passive && !this.hasPassive()) {
return false;
}
const ability = !passive ? this.getAbility() : this.getPassiveAbility();
const ability = passive ? this.getPassiveAbility() : this.getAbility();
if (this.isFusion() && ability.hasAttr("NoFusionAbilityAbAttr")) {
return false;
}
@ -2144,7 +2192,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
const suppressAbilitiesTag = arena.getTag(ArenaTagType.NEUTRALIZING_GAS) as SuppressAbilitiesTag;
const suppressOffField = ability.hasAttr("PreSummonAbAttr");
if ((this.isOnField() || suppressOffField) && suppressAbilitiesTag && !suppressAbilitiesTag.isBeingRemoved()) {
if ((this.isOnField() || suppressOffField) && suppressAbilitiesTag && !suppressAbilitiesTag.beingRemoved) {
const thisAbilitySuppressing = ability.hasAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr");
const hasSuppressingAbility = this.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false);
// Neutralizing gas is up - suppress abilities unless they are unsuppressable or this pokemon is responsible for the gas
@ -2162,13 +2210,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Checks whether a pokemon has the specified ability and it's in effect. Accounts for all the various
* effects which can affect whether an ability will be present or in effect, and both passive and
* non-passive. This is the primary way to check whether a pokemon has a particular ability.
* @param ability The ability to check for
* Check whether a pokemon has the specified ability in effect, either as a normal or passive ability.
* Accounts for all the various effects which can disable or modify abilities.
* @param ability - The {@linkcode Abilities | Ability} to check for
* @param canApply - Whether to check if the ability is currently active; default `true`
* @param ignoreOverride Whether to ignore ability changing effects; default `false`
* @returns `true` if the ability is present and active
* @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @returns Whether this {@linkcode Pokemon} has the given ability
*/
public hasAbility(ability: AbilityId, canApply = true, ignoreOverride = false): boolean {
if (this.getAbility(ignoreOverride).id === ability && (!canApply || this.canApplyAbility())) {
@ -2178,14 +2225,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Checks whether a pokemon has an ability with the specified attribute and it's in effect.
* Accounts for all the various effects which can affect whether an ability will be present or
* in effect, and both passive and non-passive. This is one of the two primary ways to check
* whether a pokemon has a particular ability.
* @param attrType The {@link AbAttr | ability attribute} to check for
* Check whether this pokemon has an ability with the specified attribute in effect, either as a normal or passive ability.
* Accounts for all the various effects which can disable or modify abilities.
* @param attrType - The {@linkcode AbAttr | attribute} to check for
* @param canApply - Whether to check if the ability is currently active; default `true`
* @param ignoreOverride Whether to ignore ability changing effects; default `false`
* @returns `true` if an ability with the given {@linkcode AbAttr} is present and active
* @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode Moves.TRANSFORM | Transform}; default `false`
* @returns Whether this Pokemon has an ability with the given {@linkcode AbAttr}.
*/
public hasAbilityWithAttr(attrType: AbAttrString, canApply = true, ignoreOverride = false): boolean {
if ((!canApply || this.canApplyAbility()) && this.getAbility(ignoreOverride).hasAttr(attrType)) {
@ -2207,7 +2252,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
const autotomizedTag = this.getTag(AutotomizedTag);
let weightRemoved = 0;
if (!isNullOrUndefined(autotomizedTag)) {
weightRemoved = 100 * autotomizedTag!.autotomizeCount;
weightRemoved = 100 * autotomizedTag.autotomizeCount;
}
const minWeight = 0.1;
const weight = new NumberHolder(this.species.weight - weightRemoved);
@ -3403,10 +3448,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
*
* Note that this does not apply to evasion or accuracy
* @see {@linkcode getAccuracyMultiplier}
* @param stat the desired {@linkcode EffectiveStat}
* @param opponent the target {@linkcode Pokemon}
* @param move the {@linkcode Move} being used
* @param ignoreOppAbility determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored (`false` by default)
* @param stat - The {@linkcode EffectiveStat} to calculate
* @param opponent - The {@linkcode Pokemon} being targeted
* @param move - The {@linkcode Move} being used
* @param ignoreOppAbility determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored (`false` by default)
* @param isCritical determines whether a critical hit has occurred or not (`false` by default)
* @param simulated determines whether effects are applied without altering game state (`true` by default)
* @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false`
@ -4395,6 +4440,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
return null;
}
/**
* Return this Pokemon's move history.
* Entries are sorted in order of OLDEST to NEWEST
* @returns An array of {@linkcode TurnMove}, as described above.
* @see {@linkcode getLastXMoves}
*/
public getMoveHistory(): TurnMove[] {
return this.summonData.moveHistory;
}
@ -4408,19 +4459,20 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Returns a list of the most recent move entries in this Pokemon's move history.
* The retrieved move entries are sorted in order from NEWEST to OLDEST.
* @param moveCount The number of move entries to retrieve.
* If negative, retrieve the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}).
* Default is `1`.
* @returns A list of {@linkcode TurnMove}, as specified above.
* Return a list of the most recent move entries in this {@linkcode Pokemon}'s move history.
* The retrieved move entries are sorted in order from **NEWEST** to **OLDEST**.
* @param moveCount - The maximum number of move entries to retrieve.
* If negative, retrieves the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}).
* Default is `1`.
* @returns An array of {@linkcode TurnMove}, as specified above.
*/
// TODO: Update documentation in dancer PR to mention "getLastNonVirtualMove"
getLastXMoves(moveCount = 1): TurnMove[] {
const moveHistory = this.getMoveHistory();
if (moveCount >= 0) {
if (moveCount > 0) {
return moveHistory.slice(Math.max(moveHistory.length - moveCount, 0)).reverse();
}
return moveHistory.slice(0).reverse();
return moveHistory.slice().reverse();
}
/**
@ -5576,13 +5628,15 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Reduces one of this Pokemon's held item stacks by 1, and removes the item if applicable.
* Reduces one of this Pokemon's held item stacks by 1, removing it if applicable.
* Does nothing if this Pokemon is somehow not the owner of the held item.
* @param heldItem The item stack to be reduced by 1.
* @param forBattle If `false`, do not trigger in-battle effects (such as Unburden) from losing the item. For example, set this to `false` if the Pokemon is giving away the held item for a Mystery Encounter. Default is `true`.
* @returns `true` if the item was removed successfully, `false` otherwise.
* @param heldItem - The item stack to be reduced.
* @param forBattle - Whether to trigger in-battle effects (such as Unburden) after losing the item. Default: `true`
* Should be `false` for all item loss occurring outside of battle (MEs, etc.).
* @returns Whether the item was removed successfully.
*/
public loseHeldItem(heldItem: PokemonHeldItemModifier, forBattle = true): boolean {
// TODO: What does a -1 pokemon id mean?
if (heldItem.pokemonId !== -1 && heldItem.pokemonId !== this.id) {
return false;
}
@ -6254,22 +6308,23 @@ export class EnemyPokemon extends Pokemon {
}
/**
* Sets the pokemons boss status. If true initializes the boss segments either from the arguments
* or through the the Scene.getEncounterBossSegments function
* Set this {@linkcode EnemyPokemon}'s boss status.
*
* @param boss if the pokemon is a boss
* @param bossSegments amount of boss segments (health-bar segments)
* @param boss - Whether this pokemon should be a boss; default `true`
* @param bossSegments - Optional amount amount of health bar segments to give;
* will be generated by {@linkcode BattleScene.getEncounterBossSegments} if omitted
*/
setBoss(boss = true, bossSegments = 0): void {
if (boss) {
this.bossSegments =
bossSegments ||
globalScene.getEncounterBossSegments(globalScene.currentBattle.waveIndex, this.level, this.species, true);
this.bossSegmentIndex = this.bossSegments - 1;
} else {
setBoss(boss = true, bossSegments?: number): void {
if (!boss) {
this.bossSegments = 0;
this.bossSegmentIndex = 0;
return;
}
this.bossSegments =
bossSegments ??
globalScene.getEncounterBossSegments(globalScene.currentBattle.waveIndex, this.level, this.species, true);
this.bossSegmentIndex = this.bossSegments - 1;
}
generateAndPopulateMoveset(formIndex?: number): void {

View File

@ -45,8 +45,10 @@ Phaser.GameObjects.Rectangle.prototype.setPositionRelative = setPositionRelative
document.fonts.load("16px emerald").then(() => document.fonts.load("10px pkmnems"));
// biome-ignore lint/suspicious/noImplicitAnyLet: TODO
let game;
// biome-ignore lint/suspicious/noImplicitAnyLet: TODO
let manifest;
const startGame = async (manifest?: any) => {
const startGame = async () => {
await initI18n();
const LoadingScene = (await import("./loading-scene")).LoadingScene;
const BattleScene = (await import("./battle-scene")).BattleScene;
@ -110,10 +112,13 @@ const startGame = async (manifest?: any) => {
fetch("/manifest.json")
.then(res => res.json())
.then(jsonResponse => {
startGame(jsonResponse.manifest);
manifest = jsonResponse.manifest;
})
.catch(() => {
// Manifest not found (likely local build)
.catch(err => {
// Manifest not found (likely local build or path error on live)
console.log(`Manifest not found. ${err}`);
})
.finally(() => {
startGame();
});

View File

@ -52,25 +52,11 @@ const iconOverflowIndex = 24;
export const modifierSortFunc = (a: Modifier, b: Modifier): number => {
const itemNameMatch = a.type.name.localeCompare(b.type.name);
const typeNameMatch = a.constructor.name.localeCompare(b.constructor.name);
const aId = a instanceof PokemonHeldItemModifier && a.pokemonId ? a.pokemonId : 4294967295;
const bId = b instanceof PokemonHeldItemModifier && b.pokemonId ? b.pokemonId : 4294967295;
const aId = a instanceof PokemonHeldItemModifier ? a.pokemonId : -1;
const bId = b instanceof PokemonHeldItemModifier ? b.pokemonId : -1;
//First sort by pokemonID
if (aId < bId) {
return 1;
}
if (aId > bId) {
return -1;
}
if (aId === bId) {
//Then sort by item type
if (typeNameMatch === 0) {
return itemNameMatch;
//Finally sort by item name
}
return typeNameMatch;
}
return 0;
// First sort by pokemon ID, then by item type and then name
return aId - bId || typeNameMatch || itemNameMatch;
};
export class ModifierBar extends Phaser.GameObjects.Container {
@ -757,7 +743,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier {
return 1;
}
getMaxStackCount(forThreshold?: boolean): number {
getMaxStackCount(forThreshold = false): number {
const pokemon = this.getPokemon();
if (!pokemon) {
return 0;
@ -2814,6 +2800,7 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier {
damageMultiplier.value *= 1 - 0.25 * this.getStackCount();
return true;
}
if (pokemon.turnData.hitCount - pokemon.turnData.hitsLeft !== this.getStackCount() + 1) {
// Deal 25% damage for each remaining Multi Lens hit
damageMultiplier.value *= 0.25;

View File

@ -562,14 +562,13 @@ export class PhaseManager {
}
/**
* Queues an ability bar flyout phase
* @param pokemon The pokemon who has the ability
* @param passive Whether the ability is a passive
* @param show Whether to show or hide the bar
* Queue a phase to show or hide the ability flyout bar.
* @param pokemon - The {@linkcode Pokemon} whose ability is being activated
* @param passive - Whether the ability is a passive
* @param show - Whether to show or hide the bar
*/
public queueAbilityDisplay(pokemon: Pokemon, passive: boolean, show: boolean): void {
this.unshiftPhase(show ? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive) : new HideAbilityPhase());
this.clearPhaseQueueSplice();
}
/**

View File

@ -88,7 +88,7 @@ export class CommandPhase extends FieldPhase {
}
// Checks if the Pokemon is under the effects of Encore. If so, Encore can end early if the encored move has no more PP.
const encoreTag = this.getPokemon().getTag(BattlerTagType.ENCORE) as EncoreTag;
const encoreTag = this.getPokemon().getTag(BattlerTagType.ENCORE) as EncoreTag | undefined;
if (encoreTag) {
this.getPokemon().lapseTag(BattlerTagType.ENCORE);
}

View File

@ -37,7 +37,7 @@ export class SelectStarterPhase extends Phase {
/**
* Initialize starters before starting the first battle
* @param starters {@linkcode Pokemon} with which to start the first battle
* @param starters - Array of {@linkcode Starter}s with which to start the battle
*/
initBattle(starters: Starter[]) {
const party = globalScene.getPlayerParty();

View File

@ -204,7 +204,7 @@ export class TitlePhase extends Phase {
globalScene.eventManager.startEventChallenges();
globalScene.setSeed(seed);
globalScene.resetSeed(0);
globalScene.resetSeed();
globalScene.money = globalScene.gameMode.getStartingMoney();
@ -283,6 +283,7 @@ export class TitlePhase extends Phase {
console.error("Failed to load daily run:\n", err);
});
} else {
// Grab first 10 chars of ISO date format (YYYY-MM-DD) and convert to base64
let seed: string = btoa(new Date().toISOString().substring(0, 10));
if (!isNullOrUndefined(Overrides.DAILY_RUN_SEED_OVERRIDE)) {
seed = Overrides.DAILY_RUN_SEED_OVERRIDE;

View File

@ -1,10 +1,20 @@
import type { ArenaTag } from "#data/arena-tag";
import { loadArenaTag } from "#data/arena-tag";
import { loadArenaTag, SerializableArenaTag } from "#data/arena-tag";
import type { SerializedPositionalTag } from "#data/positional-tags/load-positional-tag";
import { Terrain } from "#data/terrain";
import { Weather } from "#data/weather";
import type { BiomeId } from "#enums/biome-id";
import { Arena } from "#field/arena";
import type { ArenaTagTypeData } from "#types/arena-tags";
import type { NonFunctionProperties } from "#types/type-helpers";
export interface SerializedArenaData {
biome: BiomeId;
weather: NonFunctionProperties<Weather> | null;
terrain: NonFunctionProperties<Terrain> | null;
tags?: ArenaTagTypeData[];
playerTerasUsed?: number;
}
export class ArenaData {
public biome: BiomeId;
@ -14,26 +24,27 @@ export class ArenaData {
public positionalTags: SerializedPositionalTag[] = [];
public playerTerasUsed: number;
constructor(source: Arena | any) {
const sourceArena = source instanceof Arena ? (source as Arena) : null;
this.biome = sourceArena ? sourceArena.biomeType : source.biome;
this.weather = sourceArena
? sourceArena.weather
: source.weather
? new Weather(source.weather.weatherType, source.weather.turnsLeft)
: null;
this.terrain = sourceArena
? sourceArena.terrain
: source.terrain
? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft)
: null;
this.playerTerasUsed = (sourceArena ? sourceArena.playerTerasUsed : source.playerTerasUsed) ?? 0;
this.tags = [];
constructor(source: Arena | SerializedArenaData) {
// Exclude any unserializable tags from the serialized data (such as ones only lasting 1 turn).
// NOTE: The filter has to be done _after_ map, data loaded from `ArenaTagTypeData`
// is not yet an instance of `ArenaTag`
this.tags =
source.tags
?.map((t: ArenaTag | ArenaTagTypeData) => loadArenaTag(t))
?.filter((tag): tag is SerializableArenaTag => tag instanceof SerializableArenaTag) ?? [];
if (source.tags) {
this.tags = source.tags.map(t => loadArenaTag(t));
this.playerTerasUsed = source.playerTerasUsed ?? 0;
this.positionalTags = (sourceArena ? sourceArena.positionalTagManager.tags : source.positionalTags) ?? [];
if (source instanceof Arena) {
this.biome = source.biomeType;
this.weather = source.weather;
this.terrain = source.terrain;
return;
}
this.positionalTags = (sourceArena ? sourceArena.positionalTagManager.tags : source.positionalTags) ?? [];
this.biome = source.biome;
this.weather = source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : null;
this.terrain = source.terrain ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) : null;
}
}

View File

@ -43,7 +43,7 @@ import * as Modifier from "#modifiers/modifier";
import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data";
import type { Variant } from "#sprites/variant";
import { achvs } from "#system/achv";
import { ArenaData } from "#system/arena-data";
import { ArenaData, type SerializedArenaData } from "#system/arena-data";
import { ChallengeData } from "#system/challenge-data";
import { EggData } from "#system/egg-data";
import { GameStats } from "#system/game-stats";
@ -1252,7 +1252,8 @@ export class GameData {
// (or prevent them from being null)
// If the value is able to *not exist*, it should say so in the code
const sessionData = JSON.parse(dataStr, (k: string, v: any) => {
// TODO: Add pre-parse migrate scripts
// TODO: Move this to occur _after_ migrate scripts (and refactor all non-assignment duties into migrate scripts)
// This should ideally be just a giant assign block
switch (k) {
case "party":
case "enemyParty": {
@ -1290,7 +1291,7 @@ export class GameData {
}
case "arena":
return new ArenaData(v);
return new ArenaData(v as SerializedArenaData);
case "challenges": {
const ret: ChallengeData[] = [];