From 0bde4562bbd02db9043feba143e3bfa2792149c0 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Tue, 9 Sep 2025 22:46:37 -0500 Subject: [PATCH] Make abilities use builder class --- src/battle-scene.ts | 3 +- src/data/abilities/ability.ts | 1749 ++++++++++++++++++++------------- src/data/moves/move.ts | 12 +- src/field/pokemon.ts | 8 +- 4 files changed, 1087 insertions(+), 685 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index cbda368782e..9fa783cf913 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -28,7 +28,7 @@ import { biomeDepths, getBiomeName } from "#balance/biomes"; import { pokemonPrevolutions } from "#balance/pokemon-evolutions"; import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#balance/starters"; import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets } from "#data/battle-anims"; -import { allAbilities, allMoves, allSpecies, modifierTypes } from "#data/data-lists"; +import { allMoves, allSpecies, modifierTypes } from "#data/data-lists"; import { battleSpecDialogue } from "#data/dialogue"; import type { SpeciesFormChangeTrigger } from "#data/form-change-triggers"; import { SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger } from "#data/form-change-triggers"; @@ -1228,7 +1228,6 @@ export class BattleScene extends SceneBase { const localizable: Localizable[] = [ ...allSpecies, ...allMoves, - ...allAbilities, ...getEnumValues(ModifierPoolType) .map(mpt => getModifierPoolForType(mpt)) .flatMap(mp => diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index ebe8b816e5e..50d87d9a993 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -61,7 +61,6 @@ import type { PokemonDefendCondition, PokemonStatStageChangeCondition, } from "#types/ability-types"; -import type { Localizable } from "#types/locales"; import type { Closed, Exact } from "#types/type-helpers"; import type { Constructor } from "#utils/common"; import { @@ -76,43 +75,122 @@ import { import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; -export class Ability implements Localizable { - public id: AbilityId; +/** Bit set for an ability's `bypass faint` flag */ +const AB_FLAG_BYPASS_FAINT = 1; +/** Bit set for an ability's `ignorable` flag */ +const AB_FLAG_IGNORABLE = 2; +/** Bit set for an ability's `suppressable` flag */ +const AB_FLAG_UNSUPPRESSABLE = 4; +/** Bit set for an ability's `uncopiable` flag */ +const AB_FLAG_UNCOPIABLE = 8; +/** Bit set for an ability's `unreplaceable` flag */ +const AB_FLAG_UNREPLACEABLE = 16; +/** Bit set for an ability's `unimplemented` flag */ +const AB_FLAG_UNIMPLEMENTED = 32; +/** Bit set for an ability's `partial` flag */ +const AB_FLAG_PARTIAL = 64; - private nameAppend: string; - public name: string; - public description: string; - public generation: number; +/** Bits set for a swappable ability */ +const AB_FLAG_UNSWAPPABLE = AB_FLAG_UNCOPIABLE | AB_FLAG_UNREPLACEABLE; + +export class Ability { + /** The ability's unique identifier */ + public readonly id: AbilityId; + /** Key used to localize the ability's name */ + private readonly i18nKey: string; + /** The localized ability name + * @remarks + * Includes The (P) or (N) suffix, if the ability is partial/unimplemented + */ + public get name(): string { + if (this.id === AbilityId.NONE) { + return ""; + } + const name = i18next.t(`ability:${this.i18nKey}.name`); + + // TODO: Make a `getDisplayName` method (or another getter) that puts the (N) and (P) markers after the name + // Also, consider whether these markers should be localized + if (this.unimplemented) { + return name + " (N)"; + } + if (this.partial) { + return name + " (P)"; + } + return name; + } + public readonly attrs: readonly AbAttr[]; + public readonly conditions: readonly AbAttrCondition[]; + + /** The localized ability description */ + public get description(): string { + if (this.id === AbilityId.NONE) { + return ""; + } + return i18next.t(`ability:${this.i18nKey}.description`); + } + /** Whether the retains its effects through a faint */ + public get bypassFaint(): boolean { + return (this.flags & AB_FLAG_BYPASS_FAINT) !== 0; + } + /** Whether the ability is ignorable by mold breaker like effects */ + public get ignorable(): boolean { + return (this.flags & AB_FLAG_IGNORABLE) !== 0; + } + /** Whether the ability can be suppressed by gastro acid and neutralizing gas */ + public get suppressable(): boolean { + return !(this.flags & AB_FLAG_UNSUPPRESSABLE); + } + /** Whether the ability can be copied, such as via trace */ + public get copiable(): boolean { + return !(this.flags & AB_FLAG_UNCOPIABLE); + } + /** Whether the ability can be replaced, such as via entrainment */ + public get replaceable(): boolean { + return !(this.flags & AB_FLAG_UNREPLACEABLE); + } + /** Whether the ability is partially implemented. Mutually exclusive with {@linkcode unimplemented} */ + public get partial(): boolean { + return (this.flags & AB_FLAG_PARTIAL) !== 0; + } + /** Whether the ability is unimplemented. Mutually exclusive with {@linkcode partial} */ + public get unimplemented(): boolean { + return (this.flags & AB_FLAG_UNIMPLEMENTED) !== 0; + } + /** Whether this ability can be swapped via moves like skill swap */ + public get swappable(): boolean { + return !(this.flags & AB_FLAG_UNSWAPPABLE); + } + + /** The generation the ability was introduced in */ + public readonly generation: number; + + /** The ability's post summon priority order */ public readonly postSummonPriority: number; - public isBypassFaint: boolean; - public isIgnorable: boolean; - public isSuppressable = true; - public isCopiable = true; - public isReplaceable = true; - public attrs: AbAttr[]; - public conditions: AbAttrCondition[]; - constructor(id: AbilityId, generation: number, postSummonPriority = 0) { - this.id = id; + /** Holder for the ability's flags */ + protected readonly flags: number; - this.nameAppend = ""; - this.generation = generation; - this.postSummonPriority = postSummonPriority; - this.attrs = []; - this.conditions = []; - - this.localize(); + private constructor(builder: AbBuilder) { + this.id = builder.id; + this.generation = builder.generation; + this.i18nKey = toCamelCase(AbilityId[this.id]); + this.flags = builder.flags; + this.postSummonPriority = builder.postSummonPriority; + this.attrs = builder.attrs; + this.conditions = builder.conditions; } - public get isSwappable(): boolean { - return this.isCopiable && this.isReplaceable; - } - - localize(): void { - const i18nKey = toCamelCase(AbilityId[this.id]); - - this.name = this.id ? `${i18next.t(`ability:${i18nKey}.name`)}${this.nameAppend}` : ""; - this.description = this.id ? (i18next.t(`ability:${i18nKey}.description`) as string) : ""; + /** + * Check if an ability has an attribute that matches `attrType` + * @param attrType - any attribute that extends {@linkcode AbAttr} + * @returns true if the ability has attribute `attrType` + */ + hasAttr(attrType: T): boolean { + const targetAttr = AbilityAttrs[attrType]; + if (!targetAttr) { + return false; + } + return this.attrs.some(attr => attr instanceof targetAttr); } /** @@ -128,18 +206,34 @@ export class Ability implements Localizable { // TODO: figure out how to remove the `as AbAttrMap[T][]` cast return this.attrs.filter((a): a is AbAttrMap[T] => a instanceof targetAttr) as AbAttrMap[T][]; } +} + +/** + * Builder class for creating new {@linkcode Ability} instances. + */ +class AbBuilder { + public readonly id: AbilityId; + /** The generation the ability was introduced */ + public readonly generation: number; + public readonly postSummonPriority: number; + public flags = 0; + public readonly attrs: AbAttr[] = []; + public readonly conditions: AbAttrCondition[] = []; + + constructor(id: AbilityId, generation: number, postSummonPriority = 0) { + this.id = id; + this.generation = generation; + this.postSummonPriority = postSummonPriority; + } /** - * Check if an ability has an attribute that matches `attrType` - * @param attrType - any attribute that extends {@linkcode AbAttr} - * @returns true if the ability has attribute `attrType` + * Construct the ability set by this builder + * @returns A new {@linkcode Ability} instance with the parameters set in this builder. */ - hasAttr(attrType: T): boolean { - const targetAttr = AbilityAttrs[attrType]; - if (!targetAttr) { - return false; - } - return this.attrs.some(attr => attr instanceof targetAttr); + public build(): Ability { + // @ts-expect-error: Typescript doesn't support friend classes, but we only + // want this builder to be able to create new abilities. + return new Ability(this); } /** @@ -182,7 +276,7 @@ export class Ability implements Localizable { * This is also required for abilities to trigger when revived via Reviver Seed. */ bypassFaint(): this { - this.isBypassFaint = true; + this.flags |= AB_FLAG_BYPASS_FAINT; return this; } @@ -191,7 +285,7 @@ export class Ability implements Localizable { * @returns `this` */ ignorable(): this { - this.isIgnorable = true; + this.flags |= AB_FLAG_IGNORABLE; return this; } @@ -200,7 +294,7 @@ export class Ability implements Localizable { * @returns `this` */ unsuppressable(): this { - this.isSuppressable = false; + this.flags |= AB_FLAG_UNSUPPRESSABLE; return this; } @@ -209,7 +303,7 @@ export class Ability implements Localizable { * @returns `this` */ uncopiable(): this { - this.isCopiable = false; + this.flags |= AB_FLAG_UNCOPIABLE; return this; } @@ -218,7 +312,7 @@ export class Ability implements Localizable { * @returns `this` */ unreplaceable(): this { - this.isReplaceable = false; + this.flags |= AB_FLAG_UNREPLACEABLE; return this; } @@ -244,7 +338,7 @@ export class Ability implements Localizable { * @returns `this` */ partial(): this { - this.nameAppend += " (P)"; + this.flags |= AB_FLAG_PARTIAL; return this; } @@ -254,7 +348,7 @@ export class Ability implements Localizable { * @returns `this` */ unimplemented(): this { - this.nameAppend += " (N)"; + this.flags |= AB_FLAG_UNIMPLEMENTED; return this; } @@ -1411,7 +1505,7 @@ export class PostDefendAbilitySwapAbAttr extends PostDefendAbAttr { override canApply({ move, opponent: attacker, pokemon }: PostMoveInteractionAbAttrParams): boolean { return ( move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }) - && attacker.getAbility().isSwappable + && attacker.getAbility().swappable ); } @@ -1441,7 +1535,7 @@ export class PostDefendAbilityGiveAbAttr extends PostDefendAbAttr { override canApply({ move, opponent: attacker, pokemon }: PostMoveInteractionAbAttrParams): boolean { return ( move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }) - && attacker.getAbility().isSuppressable + && attacker.getAbility().suppressable && !attacker.getAbility().hasAttr("PostDefendAbilityGiveAbAttr") ); } @@ -2474,7 +2568,7 @@ export class PostKnockOutStatStageChangeAbAttr extends PostKnockOutAbAttr { export class CopyFaintedAllyAbilityAbAttr extends PostKnockOutAbAttr { override canApply({ pokemon, victim }: PostKnockOutAbAttrParams): boolean { - return pokemon.isPlayer() === victim.isPlayer() && victim.getAbility().isCopiable; + return pokemon.isPlayer() === victim.isPlayer() && victim.getAbility().copiable; } override apply({ pokemon, simulated, victim }: PostKnockOutAbAttrParams): void { @@ -3015,7 +3109,7 @@ export class PostSummonCopyAbilityAbAttr extends PostSummonAbAttr { override canApply({ pokemon, simulated }: AbAttrBaseParams): boolean { const targets = pokemon .getOpponents() - .filter(t => t.getAbility().isCopiable || t.getAbility().id === AbilityId.WONDER_GUARD); + .filter(t => t.getAbility().copiable || t.getAbility().id === AbilityId.WONDER_GUARD); if (targets.length === 0) { return false; } @@ -6705,376 +6799,497 @@ function getPokemonWithWeatherBasedForms() { // biome-ignore format: prevent biome from removing the newlines (e.g. prevent `new Ability(...).attr(...)`) export function initAbilities() { allAbilities.push( - new Ability(AbilityId.NONE, 3), - new Ability(AbilityId.STENCH, 3) - .attr(PostAttackApplyBattlerTagAbAttr, false, (user, target, move) => !move.hasAttr("FlinchAttr") && !move.hitsSubstitute(user, target) ? 10 : 0, BattlerTagType.FLINCHED), - new Ability(AbilityId.DRIZZLE, 3) + new AbBuilder(AbilityId.NONE, 3).build(), + new AbBuilder(AbilityId.STENCH, 3) + .attr(PostAttackApplyBattlerTagAbAttr, false, (user, target, move) => !move.hasAttr("FlinchAttr") && !move.hitsSubstitute(user, target) ? 10 : 0, BattlerTagType.FLINCHED) + .build(), + new AbBuilder(AbilityId.DRIZZLE, 3) .attr(PostSummonWeatherChangeAbAttr, WeatherType.RAIN) - .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.RAIN), - new Ability(AbilityId.SPEED_BOOST, 3) - .attr(SpeedBoostAbAttr), - new Ability(AbilityId.BATTLE_ARMOR, 3) + .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.RAIN) + .build(), + new AbBuilder(AbilityId.SPEED_BOOST, 3) + .attr(SpeedBoostAbAttr) + .build(), + new AbBuilder(AbilityId.BATTLE_ARMOR, 3) .attr(BlockCritAbAttr) - .ignorable(), - new Ability(AbilityId.STURDY, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.STURDY, 3) .attr(PreDefendFullHpEndureAbAttr) .attr(BlockOneHitKOAbAttr) - .ignorable(), - new Ability(AbilityId.DAMP, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.DAMP, 3) .attr(FieldPreventExplosiveMovesAbAttr) - .ignorable(), - new Ability(AbilityId.LIMBER, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.LIMBER, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.PARALYSIS) .attr(PostSummonHealStatusAbAttr, StatusEffect.PARALYSIS) - .ignorable(), - new Ability(AbilityId.SAND_VEIL, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.SAND_VEIL, 3) .attr(StatMultiplierAbAttr, Stat.EVA, 1.2) .attr(BlockWeatherDamageAttr, WeatherType.SANDSTORM) .condition(getWeatherCondition(WeatherType.SANDSTORM)) - .ignorable(), - new Ability(AbilityId.STATIC, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.STATIC, 3) .attr(PostDefendContactApplyStatusEffectAbAttr, 30, StatusEffect.PARALYSIS) - .bypassFaint(), - new Ability(AbilityId.VOLT_ABSORB, 3) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.VOLT_ABSORB, 3) .attr(TypeImmunityHealAbAttr, PokemonType.ELECTRIC) - .ignorable(), - new Ability(AbilityId.WATER_ABSORB, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.WATER_ABSORB, 3) .attr(TypeImmunityHealAbAttr, PokemonType.WATER) - .ignorable(), - new Ability(AbilityId.OBLIVIOUS, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.OBLIVIOUS, 3) .attr(BattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT ]) .attr(PostSummonRemoveBattlerTagAbAttr, BattlerTagType.INFATUATED, BattlerTagType.TAUNT) .attr(IntimidateImmunityAbAttr) - .ignorable(), - new Ability(AbilityId.CLOUD_NINE, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.CLOUD_NINE, 3) .attr(SuppressWeatherEffectAbAttr, true) .attr(PostSummonUnnamedMessageAbAttr, i18next.t("abilityTriggers:weatherEffectDisappeared")) .attr(PostSummonWeatherSuppressedFormChangeAbAttr) .attr(PostFaintUnsuppressedWeatherFormChangeAbAttr) - .bypassFaint(), - new Ability(AbilityId.COMPOUND_EYES, 3) - .attr(StatMultiplierAbAttr, Stat.ACC, 1.3), - new Ability(AbilityId.INSOMNIA, 3) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.COMPOUND_EYES, 3) + .attr(StatMultiplierAbAttr, Stat.ACC, 1.3) + .build(), + new AbBuilder(AbilityId.INSOMNIA, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP) .attr(PostSummonHealStatusAbAttr, StatusEffect.SLEEP) .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY) - .ignorable(), - new Ability(AbilityId.COLOR_CHANGE, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.COLOR_CHANGE, 3) .attr(PostDefendTypeChangeAbAttr) - .condition(getSheerForceHitDisableAbCondition()), - new Ability(AbilityId.IMMUNITY, 3) + .condition(getSheerForceHitDisableAbCondition()) + .build(), + new AbBuilder(AbilityId.IMMUNITY, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) .attr(PostSummonHealStatusAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) - .ignorable(), - new Ability(AbilityId.FLASH_FIRE, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.FLASH_FIRE, 3) .attr(TypeImmunityAddBattlerTagAbAttr, PokemonType.FIRE, BattlerTagType.FIRE_BOOST, 1) - .ignorable(), - new Ability(AbilityId.SHIELD_DUST, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.SHIELD_DUST, 3) .attr(IgnoreMoveEffectsAbAttr) - .ignorable(), - new Ability(AbilityId.OWN_TEMPO, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.OWN_TEMPO, 3) .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED) .attr(PostSummonRemoveBattlerTagAbAttr, BattlerTagType.CONFUSED) .attr(IntimidateImmunityAbAttr) - .ignorable(), - new Ability(AbilityId.SUCTION_CUPS, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.SUCTION_CUPS, 3) .attr(ForceSwitchOutImmunityAbAttr) - .ignorable(), - new Ability(AbilityId.INTIMIDATE, 3) - .attr(PostSummonStatStageChangeAbAttr, [ Stat.ATK ], -1, false, true), - new Ability(AbilityId.SHADOW_TAG, 3) - .attr(ArenaTrapAbAttr, (_user, target) => !target.hasAbility(AbilityId.SHADOW_TAG)), - new Ability(AbilityId.ROUGH_SKIN, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.INTIMIDATE, 3) + .attr(PostSummonStatStageChangeAbAttr, [ Stat.ATK ], -1, false, true) + .build(), + new AbBuilder(AbilityId.SHADOW_TAG, 3) + .attr(ArenaTrapAbAttr, (_user, target) => !target.hasAbility(AbilityId.SHADOW_TAG)) + .build(), + new AbBuilder(AbilityId.ROUGH_SKIN, 3) .attr(PostDefendContactDamageAbAttr, 8) - .bypassFaint(), - new Ability(AbilityId.WONDER_GUARD, 3) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.WONDER_GUARD, 3) .attr(NonSuperEffectiveImmunityAbAttr) .uncopiable() - .ignorable(), - new Ability(AbilityId.LEVITATE, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.LEVITATE, 3) .attr(AttackTypeImmunityAbAttr, PokemonType.GROUND, (pokemon: Pokemon) => !pokemon.getTag(GroundedTag) && !globalScene.arena.getTag(ArenaTagType.GRAVITY)) - .ignorable(), - new Ability(AbilityId.EFFECT_SPORE, 3) - .attr(EffectSporeAbAttr), - new Ability(AbilityId.SYNCHRONIZE, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.EFFECT_SPORE, 3) + .attr(EffectSporeAbAttr) + .build(), + new AbBuilder(AbilityId.SYNCHRONIZE, 3) .attr(SyncEncounterNatureAbAttr) - .attr(SynchronizeStatusAbAttr), - new Ability(AbilityId.CLEAR_BODY, 3) + .attr(SynchronizeStatusAbAttr) + .build(), + new AbBuilder(AbilityId.CLEAR_BODY, 3) .attr(ProtectStatAbAttr) - .ignorable(), - new Ability(AbilityId.NATURAL_CURE, 3) - .attr(PreSwitchOutResetStatusAbAttr), - new Ability(AbilityId.LIGHTNING_ROD, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.NATURAL_CURE, 3) + .attr(PreSwitchOutResetStatusAbAttr) + .build(), + new AbBuilder(AbilityId.LIGHTNING_ROD, 3) .attr(RedirectTypeMoveAbAttr, PokemonType.ELECTRIC) .attr(TypeImmunityStatStageChangeAbAttr, PokemonType.ELECTRIC, Stat.SPATK, 1) - .ignorable(), - new Ability(AbilityId.SERENE_GRACE, 3) - .attr(MoveEffectChanceMultiplierAbAttr, 2), - new Ability(AbilityId.SWIFT_SWIM, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.SERENE_GRACE, 3) + .attr(MoveEffectChanceMultiplierAbAttr, 2) + .build(), + new AbBuilder(AbilityId.SWIFT_SWIM, 3) .attr(StatMultiplierAbAttr, Stat.SPD, 2) - .condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)), - new Ability(AbilityId.CHLOROPHYLL, 3) + .condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)) + .build(), + new AbBuilder(AbilityId.CHLOROPHYLL, 3) .attr(StatMultiplierAbAttr, Stat.SPD, 2) - .condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)), - new Ability(AbilityId.ILLUMINATE, 3) + .condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)) + .build(), + new AbBuilder(AbilityId.ILLUMINATE, 3) .attr(ProtectStatAbAttr, Stat.ACC) .attr(DoubleBattleChanceAbAttr) .attr(IgnoreOpponentStatStagesAbAttr, [ Stat.EVA ]) - .ignorable(), - new Ability(AbilityId.TRACE, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.TRACE, 3) .attr(PostSummonCopyAbilityAbAttr) - .uncopiable(), - new Ability(AbilityId.HUGE_POWER, 3) - .attr(StatMultiplierAbAttr, Stat.ATK, 2), - new Ability(AbilityId.POISON_POINT, 3) + .uncopiable() + .build(), + new AbBuilder(AbilityId.HUGE_POWER, 3) + .attr(StatMultiplierAbAttr, Stat.ATK, 2) + .build(), + new AbBuilder(AbilityId.POISON_POINT, 3) .attr(PostDefendContactApplyStatusEffectAbAttr, 30, StatusEffect.POISON) - .bypassFaint(), - new Ability(AbilityId.INNER_FOCUS, 3) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.INNER_FOCUS, 3) .attr(BattlerTagImmunityAbAttr, BattlerTagType.FLINCHED) .attr(IntimidateImmunityAbAttr) - .ignorable(), - new Ability(AbilityId.MAGMA_ARMOR, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.MAGMA_ARMOR, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.FREEZE) .attr(PostSummonHealStatusAbAttr, StatusEffect.FREEZE) - .ignorable(), - new Ability(AbilityId.WATER_VEIL, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.WATER_VEIL, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.BURN) .attr(PostSummonHealStatusAbAttr, StatusEffect.BURN) - .ignorable(), - new Ability(AbilityId.MAGNET_PULL, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.MAGNET_PULL, 3) .attr(ArenaTrapAbAttr, (_user, target) => { return target.getTypes(true).includes(PokemonType.STEEL) || (target.getTypes(true).includes(PokemonType.STELLAR) && target.getTypes().includes(PokemonType.STEEL)); - }), - new Ability(AbilityId.SOUNDPROOF, 3) + }) + .build(), + new AbBuilder(AbilityId.SOUNDPROOF, 3) .attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.hasFlag(MoveFlags.SOUND_BASED)) - .ignorable(), - new Ability(AbilityId.RAIN_DISH, 3) - .attr(PostWeatherLapseHealAbAttr, 1, WeatherType.RAIN, WeatherType.HEAVY_RAIN), - new Ability(AbilityId.SAND_STREAM, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.RAIN_DISH, 3) + .attr(PostWeatherLapseHealAbAttr, 1, WeatherType.RAIN, WeatherType.HEAVY_RAIN) + .build(), + new AbBuilder(AbilityId.SAND_STREAM, 3) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SANDSTORM) - .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SANDSTORM), - new Ability(AbilityId.PRESSURE, 3) + .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SANDSTORM) + .build(), + new AbBuilder(AbilityId.PRESSURE, 3) .attr(IncreasePpAbAttr) - .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonPressure", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })), - new Ability(AbilityId.THICK_FAT, 3) + .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonPressure", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) + .build(), + new AbBuilder(AbilityId.THICK_FAT, 3) .attr(ReceivedTypeDamageMultiplierAbAttr, PokemonType.FIRE, 0.5) .attr(ReceivedTypeDamageMultiplierAbAttr, PokemonType.ICE, 0.5) - .ignorable(), - new Ability(AbilityId.EARLY_BIRD, 3) - .attr(ReduceStatusEffectDurationAbAttr, StatusEffect.SLEEP), - new Ability(AbilityId.FLAME_BODY, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.EARLY_BIRD, 3) + .attr(ReduceStatusEffectDurationAbAttr, StatusEffect.SLEEP) + .build(), + new AbBuilder(AbilityId.FLAME_BODY, 3) .attr(PostDefendContactApplyStatusEffectAbAttr, 30, StatusEffect.BURN) - .bypassFaint(), - new Ability(AbilityId.RUN_AWAY, 3) - .attr(RunSuccessAbAttr), - new Ability(AbilityId.KEEN_EYE, 3) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.RUN_AWAY, 3) + .attr(RunSuccessAbAttr) + .build(), + new AbBuilder(AbilityId.KEEN_EYE, 3) .attr(ProtectStatAbAttr, Stat.ACC) - .ignorable(), - new Ability(AbilityId.HYPER_CUTTER, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.HYPER_CUTTER, 3) .attr(ProtectStatAbAttr, Stat.ATK) - .ignorable(), - new Ability(AbilityId.PICKUP, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.PICKUP, 3) .attr(PostBattleLootAbAttr) - .unsuppressable(), - new Ability(AbilityId.TRUANT, 3) - .attr(PostSummonAddBattlerTagAbAttr, BattlerTagType.TRUANT, 1, false), - new Ability(AbilityId.HUSTLE, 3) + .unsuppressable() + .build(), + new AbBuilder(AbilityId.TRUANT, 3) + .attr(PostSummonAddBattlerTagAbAttr, BattlerTagType.TRUANT, 1, false) + .build(), + new AbBuilder(AbilityId.HUSTLE, 3) .attr(StatMultiplierAbAttr, Stat.ATK, 1.5) - .attr(StatMultiplierAbAttr, Stat.ACC, 0.8, (_user, _target, move) => move.category === MoveCategory.PHYSICAL), - new Ability(AbilityId.CUTE_CHARM, 3) - .attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED), - new Ability(AbilityId.PLUS, 3) - .conditionalAttr(p => globalScene.currentBattle.double && [ AbilityId.PLUS, AbilityId.MINUS ].some(a => (p.getAlly()?.hasAbility(a) ?? false)), StatMultiplierAbAttr, Stat.SPATK, 1.5), - new Ability(AbilityId.MINUS, 3) - .conditionalAttr(p => globalScene.currentBattle.double && [ AbilityId.PLUS, AbilityId.MINUS ].some(a => (p.getAlly()?.hasAbility(a) ?? false)), StatMultiplierAbAttr, Stat.SPATK, 1.5), - new Ability(AbilityId.FORECAST, 3, -2) + .attr(StatMultiplierAbAttr, Stat.ACC, 0.8, (_user, _target, move) => move.category === MoveCategory.PHYSICAL) + .build(), + new AbBuilder(AbilityId.CUTE_CHARM, 3) + .attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED) + .build(), + new AbBuilder(AbilityId.PLUS, 3) + .conditionalAttr(p => globalScene.currentBattle.double && [ AbilityId.PLUS, AbilityId.MINUS ].some(a => (p.getAlly()?.hasAbility(a) ?? false)), StatMultiplierAbAttr, Stat.SPATK, 1.5) + .build(), + new AbBuilder(AbilityId.MINUS, 3) + .conditionalAttr(p => globalScene.currentBattle.double && [ AbilityId.PLUS, AbilityId.MINUS ].some(a => (p.getAlly()?.hasAbility(a) ?? false)), StatMultiplierAbAttr, Stat.SPATK, 1.5) + .build(), + new AbBuilder(AbilityId.FORECAST, 3, -2) .uncopiable() .unreplaceable() .attr(NoFusionAbilityAbAttr) .attr(PostSummonFormChangeByWeatherAbAttr, AbilityId.FORECAST) - .attr(PostWeatherChangeFormChangeAbAttr, AbilityId.FORECAST, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG ]), - new Ability(AbilityId.STICKY_HOLD, 3) + .attr(PostWeatherChangeFormChangeAbAttr, AbilityId.FORECAST, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG ]) + .build(), + new AbBuilder(AbilityId.STICKY_HOLD, 3) .attr(BlockItemTheftAbAttr) .bypassFaint() - .ignorable(), - new Ability(AbilityId.SHED_SKIN, 3) - .conditionalAttr(_pokemon => !randSeedInt(3), PostTurnResetStatusAbAttr), - new Ability(AbilityId.GUTS, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.SHED_SKIN, 3) + .conditionalAttr(_pokemon => !randSeedInt(3), PostTurnResetStatusAbAttr) + .build(), + new AbBuilder(AbilityId.GUTS, 3) .attr(BypassBurnDamageReductionAbAttr) - .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(AbilityId.COMATOSE), StatMultiplierAbAttr, Stat.ATK, 1.5), - new Ability(AbilityId.MARVEL_SCALE, 3) + .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(AbilityId.COMATOSE), StatMultiplierAbAttr, Stat.ATK, 1.5) + .build(), + new AbBuilder(AbilityId.MARVEL_SCALE, 3) .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(AbilityId.COMATOSE), StatMultiplierAbAttr, Stat.DEF, 1.5) - .ignorable(), - new Ability(AbilityId.LIQUID_OOZE, 3) - .attr(ReverseDrainAbAttr), - new Ability(AbilityId.OVERGROW, 3) - .attr(LowHpMoveTypePowerBoostAbAttr, PokemonType.GRASS), - new Ability(AbilityId.BLAZE, 3) - .attr(LowHpMoveTypePowerBoostAbAttr, PokemonType.FIRE), - new Ability(AbilityId.TORRENT, 3) - .attr(LowHpMoveTypePowerBoostAbAttr, PokemonType.WATER), - new Ability(AbilityId.SWARM, 3) - .attr(LowHpMoveTypePowerBoostAbAttr, PokemonType.BUG), - new Ability(AbilityId.ROCK_HEAD, 3) - .attr(BlockRecoilDamageAttr), - new Ability(AbilityId.DROUGHT, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.LIQUID_OOZE, 3) + .attr(ReverseDrainAbAttr) + .build(), + new AbBuilder(AbilityId.OVERGROW, 3) + .attr(LowHpMoveTypePowerBoostAbAttr, PokemonType.GRASS) + .build(), + new AbBuilder(AbilityId.BLAZE, 3) + .attr(LowHpMoveTypePowerBoostAbAttr, PokemonType.FIRE) + .build(), + new AbBuilder(AbilityId.TORRENT, 3) + .attr(LowHpMoveTypePowerBoostAbAttr, PokemonType.WATER) + .build(), + new AbBuilder(AbilityId.SWARM, 3) + .attr(LowHpMoveTypePowerBoostAbAttr, PokemonType.BUG) + .build(), + new AbBuilder(AbilityId.ROCK_HEAD, 3) + .attr(BlockRecoilDamageAttr) + .build(), + new AbBuilder(AbilityId.DROUGHT, 3) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY) - .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY), - new Ability(AbilityId.ARENA_TRAP, 3) + .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY) + .build(), + new AbBuilder(AbilityId.ARENA_TRAP, 3) .attr(ArenaTrapAbAttr, (_user, target) => target.isGrounded()) - .attr(DoubleBattleChanceAbAttr), - new Ability(AbilityId.VITAL_SPIRIT, 3) + .attr(DoubleBattleChanceAbAttr) + .build(), + new AbBuilder(AbilityId.VITAL_SPIRIT, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP) .attr(PostSummonHealStatusAbAttr, StatusEffect.SLEEP) .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY) - .ignorable(), - new Ability(AbilityId.WHITE_SMOKE, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.WHITE_SMOKE, 3) .attr(ProtectStatAbAttr) - .ignorable(), - new Ability(AbilityId.PURE_POWER, 3) - .attr(StatMultiplierAbAttr, Stat.ATK, 2), - new Ability(AbilityId.SHELL_ARMOR, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.PURE_POWER, 3) + .attr(StatMultiplierAbAttr, Stat.ATK, 2) + .build(), + new AbBuilder(AbilityId.SHELL_ARMOR, 3) .attr(BlockCritAbAttr) - .ignorable(), - new Ability(AbilityId.AIR_LOCK, 3) + .ignorable() + .build(), + new AbBuilder(AbilityId.AIR_LOCK, 3) .attr(SuppressWeatherEffectAbAttr, true) .attr(PostSummonUnnamedMessageAbAttr, i18next.t("abilityTriggers:weatherEffectDisappeared")) .attr(PostSummonWeatherSuppressedFormChangeAbAttr) .attr(PostFaintUnsuppressedWeatherFormChangeAbAttr) - .bypassFaint(), - new Ability(AbilityId.TANGLED_FEET, 4) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.TANGLED_FEET, 4) .conditionalAttr(pokemon => !!pokemon.getTag(BattlerTagType.CONFUSED), StatMultiplierAbAttr, Stat.EVA, 2) - .ignorable(), - new Ability(AbilityId.MOTOR_DRIVE, 4) + .ignorable() + .build(), + new AbBuilder(AbilityId.MOTOR_DRIVE, 4) .attr(TypeImmunityStatStageChangeAbAttr, PokemonType.ELECTRIC, Stat.SPD, 1) - .ignorable(), - new Ability(AbilityId.RIVALRY, 4) + .ignorable() + .build(), + new AbBuilder(AbilityId.RIVALRY, 4) .attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender === target?.gender, 1.25) - .attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender !== target?.gender, 0.75), - new Ability(AbilityId.STEADFAST, 4) - .attr(FlinchStatStageChangeAbAttr, [ Stat.SPD ], 1), - new Ability(AbilityId.SNOW_CLOAK, 4) + .attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender !== target?.gender, 0.75) + .build(), + new AbBuilder(AbilityId.STEADFAST, 4) + .attr(FlinchStatStageChangeAbAttr, [ Stat.SPD ], 1) + .build(), + new AbBuilder(AbilityId.SNOW_CLOAK, 4) .attr(StatMultiplierAbAttr, Stat.EVA, 1.2) .attr(BlockWeatherDamageAttr, WeatherType.HAIL) .condition(getWeatherCondition(WeatherType.HAIL, WeatherType.SNOW)) - .ignorable(), - new Ability(AbilityId.GLUTTONY, 4) - .attr(ReduceBerryUseThresholdAbAttr), - new Ability(AbilityId.ANGER_POINT, 4) - .attr(PostReceiveCritStatStageChangeAbAttr, Stat.ATK, 12), - new Ability(AbilityId.UNBURDEN, 4) + .ignorable() + .build(), + new AbBuilder(AbilityId.GLUTTONY, 4) + .attr(ReduceBerryUseThresholdAbAttr) + .build(), + new AbBuilder(AbilityId.ANGER_POINT, 4) + .attr(PostReceiveCritStatStageChangeAbAttr, Stat.ATK, 12) + .build(), + new AbBuilder(AbilityId.UNBURDEN, 4) .attr(PostItemLostApplyBattlerTagAbAttr, BattlerTagType.UNBURDEN) .bypassFaint() // Allows reviver seed to activate Unburden - .edgeCase(), // Should not restore Unburden boost if Pokemon loses then regains Unburden ability - new Ability(AbilityId.HEATPROOF, 4) + .edgeCase() // Should not restore Unburden boost if Pokemon loses then regains Unburden ability + .build(), + new AbBuilder(AbilityId.HEATPROOF, 4) .attr(ReceivedTypeDamageMultiplierAbAttr, PokemonType.FIRE, 0.5) .attr(ReduceBurnDamageAbAttr, 0.5) - .ignorable(), - new Ability(AbilityId.SIMPLE, 4) + .ignorable() + .build(), + new AbBuilder(AbilityId.SIMPLE, 4) .attr(StatStageChangeMultiplierAbAttr, 2) - .ignorable(), - new Ability(AbilityId.DRY_SKIN, 4) + .ignorable() + .build(), + new AbBuilder(AbilityId.DRY_SKIN, 4) .attr(PostWeatherLapseDamageAbAttr, 2, WeatherType.SUNNY, WeatherType.HARSH_SUN) .attr(PostWeatherLapseHealAbAttr, 2, WeatherType.RAIN, WeatherType.HEAVY_RAIN) .attr(ReceivedTypeDamageMultiplierAbAttr, PokemonType.FIRE, 1.25) .attr(TypeImmunityHealAbAttr, PokemonType.WATER) - .ignorable(), - new Ability(AbilityId.DOWNLOAD, 4) - .attr(DownloadAbAttr), - new Ability(AbilityId.IRON_FIST, 4) - .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.PUNCHING_MOVE), 1.2), - new Ability(AbilityId.POISON_HEAL, 4) + .ignorable() + .build(), + new AbBuilder(AbilityId.DOWNLOAD, 4) + .attr(DownloadAbAttr) + .build(), + new AbBuilder(AbilityId.IRON_FIST, 4) + .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.PUNCHING_MOVE), 1.2) + .build(), + new AbBuilder(AbilityId.POISON_HEAL, 4) .attr(PostTurnStatusHealAbAttr, StatusEffect.TOXIC, StatusEffect.POISON) - .attr(BlockStatusDamageAbAttr, StatusEffect.TOXIC, StatusEffect.POISON), - new Ability(AbilityId.ADAPTABILITY, 4) - .attr(StabBoostAbAttr), - new Ability(AbilityId.SKILL_LINK, 4) - .attr(MaxMultiHitAbAttr), - new Ability(AbilityId.HYDRATION, 4) + .attr(BlockStatusDamageAbAttr, StatusEffect.TOXIC, StatusEffect.POISON) + .build(), + new AbBuilder(AbilityId.ADAPTABILITY, 4) + .attr(StabBoostAbAttr) + .build(), + new AbBuilder(AbilityId.SKILL_LINK, 4) + .attr(MaxMultiHitAbAttr) + .build(), + new AbBuilder(AbilityId.HYDRATION, 4) .attr(PostTurnResetStatusAbAttr) - .condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)), - new Ability(AbilityId.SOLAR_POWER, 4) + .condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)) + .build(), + new AbBuilder(AbilityId.SOLAR_POWER, 4) .attr(PostWeatherLapseDamageAbAttr, 2, WeatherType.SUNNY, WeatherType.HARSH_SUN) .attr(StatMultiplierAbAttr, Stat.SPATK, 1.5) - .condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)), - new Ability(AbilityId.QUICK_FEET, 4) + .condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)) + .build(), + new AbBuilder(AbilityId.QUICK_FEET, 4) // TODO: This should ignore the speed drop, not manually undo it .conditionalAttr(pokemon => pokemon.status ? pokemon.status.effect === StatusEffect.PARALYSIS : false, StatMultiplierAbAttr, Stat.SPD, 2) - .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(AbilityId.COMATOSE), StatMultiplierAbAttr, Stat.SPD, 1.5), - new Ability(AbilityId.NORMALIZE, 4) - .attr(MoveTypeChangeAbAttr, PokemonType.NORMAL, 1.2), - new Ability(AbilityId.SNIPER, 4) - .attr(MultCritAbAttr, 1.5), - new Ability(AbilityId.MAGIC_GUARD, 4) - .attr(BlockNonDirectDamageAbAttr), - new Ability(AbilityId.NO_GUARD, 4) + .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(AbilityId.COMATOSE), StatMultiplierAbAttr, Stat.SPD, 1.5) + .build(), + new AbBuilder(AbilityId.NORMALIZE, 4) + .attr(MoveTypeChangeAbAttr, PokemonType.NORMAL, 1.2) + .build(), + new AbBuilder(AbilityId.SNIPER, 4) + .attr(MultCritAbAttr, 1.5) + .build(), + new AbBuilder(AbilityId.MAGIC_GUARD, 4) + .attr(BlockNonDirectDamageAbAttr) + .build(), + new AbBuilder(AbilityId.NO_GUARD, 4) .attr(AlwaysHitAbAttr) - .attr(DoubleBattleChanceAbAttr), - new Ability(AbilityId.STALL, 4) - .attr(ChangeMovePriorityAbAttr, (_pokemon, _move: Move) => true, -0.2), - new Ability(AbilityId.TECHNICIAN, 4) + .attr(DoubleBattleChanceAbAttr) + .build(), + new AbBuilder(AbilityId.STALL, 4) + .attr(ChangeMovePriorityAbAttr, (_pokemon, _move: Move) => true, -0.2) + .build(), + new AbBuilder(AbilityId.TECHNICIAN, 4) .attr(MovePowerBoostAbAttr, (user, target, move) => { const power = new NumberHolder(move.power); applyMoveAttrs("VariablePowerAttr", user, target, move, power); return power.value <= 60; - }, 1.5), - new Ability(AbilityId.LEAF_GUARD, 4) + }, 1.5) + .build(), + new AbBuilder(AbilityId.LEAF_GUARD, 4) .attr(StatusEffectImmunityAbAttr) .condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)) - .ignorable(), - new Ability(AbilityId.KLUTZ, 4, 1) - .unimplemented(), - new Ability(AbilityId.MOLD_BREAKER, 4) + .ignorable() + .build(), + new AbBuilder(AbilityId.KLUTZ, 4, 1) + .unimplemented() + .build(), + new AbBuilder(AbilityId.MOLD_BREAKER, 4) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonMoldBreaker", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) - .attr(MoveAbilityBypassAbAttr), - new Ability(AbilityId.SUPER_LUCK, 4) - .attr(BonusCritAbAttr), - new Ability(AbilityId.AFTERMATH, 4) + .attr(MoveAbilityBypassAbAttr) + .build(), + new AbBuilder(AbilityId.SUPER_LUCK, 4) + .attr(BonusCritAbAttr) + .build(), + new AbBuilder(AbilityId.AFTERMATH, 4) .attr(PostFaintContactDamageAbAttr, 4) - .bypassFaint(), - new Ability(AbilityId.ANTICIPATION, 4) - .conditionalAttr(getAnticipationCondition(), PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAnticipation", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })), - new Ability(AbilityId.FOREWARN, 4) - .attr(ForewarnAbAttr), - new Ability(AbilityId.UNAWARE, 4) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.ANTICIPATION, 4) + .conditionalAttr(getAnticipationCondition(), PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAnticipation", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) + .build(), + new AbBuilder(AbilityId.FOREWARN, 4) + .attr(ForewarnAbAttr) + .build(), + new AbBuilder(AbilityId.UNAWARE, 4) .attr(IgnoreOpponentStatStagesAbAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA ]) - .ignorable(), - new Ability(AbilityId.TINTED_LENS, 4) - .attr(DamageBoostAbAttr, 2, (user, target, move) => (target?.getMoveEffectiveness(user!, move) ?? 1) <= 0.5), - new Ability(AbilityId.FILTER, 4) + .ignorable() + .build(), + new AbBuilder(AbilityId.TINTED_LENS, 4) + .attr(DamageBoostAbAttr, 2, (user, target, move) => (target?.getMoveEffectiveness(user!, move) ?? 1) <= 0.5) + .build(), + new AbBuilder(AbilityId.FILTER, 4) .attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getMoveEffectiveness(user, move) >= 2, 0.75) - .ignorable(), - new Ability(AbilityId.SLOW_START, 4) - .attr(PostSummonAddBattlerTagAbAttr, BattlerTagType.SLOW_START, 5), - new Ability(AbilityId.SCRAPPY, 4) + .ignorable() + .build(), + new AbBuilder(AbilityId.SLOW_START, 4) + .attr(PostSummonAddBattlerTagAbAttr, BattlerTagType.SLOW_START, 5) + .build(), + new AbBuilder(AbilityId.SCRAPPY, 4) .attr(IgnoreTypeImmunityAbAttr, PokemonType.GHOST, [ PokemonType.NORMAL, PokemonType.FIGHTING ]) - .attr(IntimidateImmunityAbAttr), - new Ability(AbilityId.STORM_DRAIN, 4) + .attr(IntimidateImmunityAbAttr) + .build(), + new AbBuilder(AbilityId.STORM_DRAIN, 4) .attr(RedirectTypeMoveAbAttr, PokemonType.WATER) .attr(TypeImmunityStatStageChangeAbAttr, PokemonType.WATER, Stat.SPATK, 1) - .ignorable(), - new Ability(AbilityId.ICE_BODY, 4) + .ignorable() + .build(), + new AbBuilder(AbilityId.ICE_BODY, 4) .attr(BlockWeatherDamageAttr, WeatherType.HAIL) - .attr(PostWeatherLapseHealAbAttr, 1, WeatherType.HAIL, WeatherType.SNOW), - new Ability(AbilityId.SOLID_ROCK, 4) + .attr(PostWeatherLapseHealAbAttr, 1, WeatherType.HAIL, WeatherType.SNOW) + .build(), + new AbBuilder(AbilityId.SOLID_ROCK, 4) .attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getMoveEffectiveness(user, move) >= 2, 0.75) - .ignorable(), - new Ability(AbilityId.SNOW_WARNING, 4) + .ignorable() + .build(), + new AbBuilder(AbilityId.SNOW_WARNING, 4) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SNOW) - .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SNOW), - new Ability(AbilityId.HONEY_GATHER, 4) + .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SNOW) + .build(), + new AbBuilder(AbilityId.HONEY_GATHER, 4) .attr(MoneyAbAttr) - .unsuppressable(), - new Ability(AbilityId.FRISK, 4) - .attr(FriskAbAttr), - new Ability(AbilityId.RECKLESS, 4) - .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.RECKLESS_MOVE), 1.2), - new Ability(AbilityId.MULTITYPE, 4) + .unsuppressable() + .build(), + new AbBuilder(AbilityId.FRISK, 4) + .attr(FriskAbAttr) + .build(), + new AbBuilder(AbilityId.RECKLESS, 4) + .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.RECKLESS_MOVE), 1.2) + .build(), + new AbBuilder(AbilityId.MULTITYPE, 4) .attr(NoFusionAbilityAbAttr) .uncopiable() .unsuppressable() - .unreplaceable(), - new Ability(AbilityId.FLOWER_GIFT, 4, -2) + .unreplaceable() + .build(), + new AbBuilder(AbilityId.FLOWER_GIFT, 4, -2) .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.ATK, 1.5) .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.SPDEF, 1.5) .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), AllyStatMultiplierAbAttr, Stat.ATK, 1.5) @@ -7084,86 +7299,113 @@ export function initAbilities() { .attr(PostWeatherChangeFormChangeAbAttr, AbilityId.FLOWER_GIFT, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HAIL, WeatherType.HEAVY_RAIN, WeatherType.SNOW, WeatherType.RAIN ]) .uncopiable() .unreplaceable() - .ignorable(), - new Ability(AbilityId.BAD_DREAMS, 4) - .attr(PostTurnHurtIfSleepingAbAttr), - new Ability(AbilityId.PICKPOCKET, 5) + .ignorable() + .build(), + new AbBuilder(AbilityId.BAD_DREAMS, 4) + .attr(PostTurnHurtIfSleepingAbAttr) + .build(), + new AbBuilder(AbilityId.PICKPOCKET, 5) .attr(PostDefendStealHeldItemAbAttr, (target, user, move) => move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user, target})) - .condition(getSheerForceHitDisableAbCondition()), - new Ability(AbilityId.SHEER_FORCE, 5) + .condition(getSheerForceHitDisableAbCondition()) + .build(), + new AbBuilder(AbilityId.SHEER_FORCE, 5) .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.chance >= 1, 1.3) - .attr(MoveEffectChanceMultiplierAbAttr, 0), // This attribute does not seem to function - Should disable life orb, eject button, red card, kee/maranga berry if they get implemented - new Ability(AbilityId.CONTRARY, 5) + .attr(MoveEffectChanceMultiplierAbAttr, 0) // This attribute does not seem to function - Should disable life orb, eject button, red card, kee/maranga berry if they get implemented + .build(), + new AbBuilder(AbilityId.CONTRARY, 5) .attr(StatStageChangeMultiplierAbAttr, -1) - .ignorable(), - new Ability(AbilityId.UNNERVE, 5, 1) - .attr(PreventBerryUseAbAttr), - new Ability(AbilityId.DEFIANT, 5) - .attr(PostStatStageChangeStatStageChangeAbAttr, (_target, _statsChanged, stages) => stages < 0, [ Stat.ATK ], 2), - new Ability(AbilityId.DEFEATIST, 5) + .ignorable() + .build(), + new AbBuilder(AbilityId.UNNERVE, 5, 1) + .attr(PreventBerryUseAbAttr) + .build(), + new AbBuilder(AbilityId.DEFIANT, 5) + .attr(PostStatStageChangeStatStageChangeAbAttr, (_target, _statsChanged, stages) => stages < 0, [ Stat.ATK ], 2) + .build(), + new AbBuilder(AbilityId.DEFEATIST, 5) .attr(StatMultiplierAbAttr, Stat.ATK, 0.5) .attr(StatMultiplierAbAttr, Stat.SPATK, 0.5) - .condition((pokemon) => pokemon.getHpRatio() <= 0.5), - new Ability(AbilityId.CURSED_BODY, 5) + .condition((pokemon) => pokemon.getHpRatio() <= 0.5) + .build(), + new AbBuilder(AbilityId.CURSED_BODY, 5) .attr(PostDefendMoveDisableAbAttr, 30) - .bypassFaint(), - new Ability(AbilityId.HEALER, 5) - .conditionalAttr(pokemon => pokemon.getAlly() != null && randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true), - new Ability(AbilityId.FRIEND_GUARD, 5) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.HEALER, 5) + .conditionalAttr(pokemon => pokemon.getAlly() != null && randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true) + .build(), + new AbBuilder(AbilityId.FRIEND_GUARD, 5) .attr(AlliedFieldDamageReductionAbAttr, 0.75) - .ignorable(), - new Ability(AbilityId.WEAK_ARMOR, 5) + .ignorable() + .build(), + new AbBuilder(AbilityId.WEAK_ARMOR, 5) .attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => move.category === MoveCategory.PHYSICAL, Stat.DEF, -1) - .attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => move.category === MoveCategory.PHYSICAL, Stat.SPD, 2), - new Ability(AbilityId.HEAVY_METAL, 5) + .attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => move.category === MoveCategory.PHYSICAL, Stat.SPD, 2) + .build(), + new AbBuilder(AbilityId.HEAVY_METAL, 5) .attr(WeightMultiplierAbAttr, 2) - .ignorable(), - new Ability(AbilityId.LIGHT_METAL, 5) + .ignorable() + .build(), + new AbBuilder(AbilityId.LIGHT_METAL, 5) .attr(WeightMultiplierAbAttr, 0.5) - .ignorable(), - new Ability(AbilityId.MULTISCALE, 5) + .ignorable() + .build(), + new AbBuilder(AbilityId.MULTISCALE, 5) .attr(ReceivedMoveDamageMultiplierAbAttr, (target, _user, _move) => target.isFullHp(), 0.5) - .ignorable(), - new Ability(AbilityId.TOXIC_BOOST, 5) - .attr(MovePowerBoostAbAttr, (user, _target, move) => move.category === MoveCategory.PHYSICAL && (user?.status?.effect === StatusEffect.POISON || user?.status?.effect === StatusEffect.TOXIC), 1.5), - new Ability(AbilityId.FLARE_BOOST, 5) - .attr(MovePowerBoostAbAttr, (user, _target, move) => move.category === MoveCategory.SPECIAL && user?.status?.effect === StatusEffect.BURN, 1.5), - new Ability(AbilityId.HARVEST, 5) + .ignorable() + .build(), + new AbBuilder(AbilityId.TOXIC_BOOST, 5) + .attr(MovePowerBoostAbAttr, (user, _target, move) => move.category === MoveCategory.PHYSICAL && (user?.status?.effect === StatusEffect.POISON || user?.status?.effect === StatusEffect.TOXIC), 1.5) + .build(), + new AbBuilder(AbilityId.FLARE_BOOST, 5) + .attr(MovePowerBoostAbAttr, (user, _target, move) => move.category === MoveCategory.SPECIAL && user?.status?.effect === StatusEffect.BURN, 1.5) + .build(), + new AbBuilder(AbilityId.HARVEST, 5) .attr( PostTurnRestoreBerryAbAttr, /** Rate is doubled when under sun {@link https://dex.pokemonshowdown.com/abilities/harvest} */ (pokemon) => 0.5 * (getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)(pokemon) ? 2 : 1) ) - .edgeCase(), // Cannot recover berries used up by fling or natural gift (unimplemented) - new Ability(AbilityId.TELEPATHY, 5) + .edgeCase() // Cannot recover berries used up by fling or natural gift (unimplemented) + .build(), + new AbBuilder(AbilityId.TELEPATHY, 5) .attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon.getAlly() === attacker && move.is("AttackMove")) - .ignorable(), - new Ability(AbilityId.MOODY, 5) - .attr(MoodyAbAttr), - new Ability(AbilityId.OVERCOAT, 5) + .ignorable() + .build(), + new AbBuilder(AbilityId.MOODY, 5) + .attr(MoodyAbAttr) + .build(), + new AbBuilder(AbilityId.OVERCOAT, 5) .attr(BlockWeatherDamageAttr) .attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.hasFlag(MoveFlags.POWDER_MOVE)) - .ignorable(), - new Ability(AbilityId.POISON_TOUCH, 5) - .attr(PostAttackContactApplyStatusEffectAbAttr, 30, StatusEffect.POISON), - new Ability(AbilityId.REGENERATOR, 5) - .attr(PreSwitchOutHealAbAttr), - new Ability(AbilityId.BIG_PECKS, 5) + .ignorable() + .build(), + new AbBuilder(AbilityId.POISON_TOUCH, 5) + .attr(PostAttackContactApplyStatusEffectAbAttr, 30, StatusEffect.POISON) + .build(), + new AbBuilder(AbilityId.REGENERATOR, 5) + .attr(PreSwitchOutHealAbAttr) + .build(), + new AbBuilder(AbilityId.BIG_PECKS, 5) .attr(ProtectStatAbAttr, Stat.DEF) - .ignorable(), - new Ability(AbilityId.SAND_RUSH, 5) + .ignorable() + .build(), + new AbBuilder(AbilityId.SAND_RUSH, 5) .attr(StatMultiplierAbAttr, Stat.SPD, 2) .attr(BlockWeatherDamageAttr, WeatherType.SANDSTORM) - .condition(getWeatherCondition(WeatherType.SANDSTORM)), - new Ability(AbilityId.WONDER_SKIN, 5) + .condition(getWeatherCondition(WeatherType.SANDSTORM)) + .build(), + new AbBuilder(AbilityId.WONDER_SKIN, 5) .attr(WonderSkinAbAttr) - .ignorable(), - new Ability(AbilityId.ANALYTIC, 5) + .ignorable() + .build(), + new AbBuilder(AbilityId.ANALYTIC, 5) .attr(MovePowerBoostAbAttr, (user) => // Boost power if all other Pokemon have already moved (no other moves are slated to execute) !globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id), - 1.3), - new Ability(AbilityId.ILLUSION, 5) + 1.3) + .build(), + new AbBuilder(AbilityId.ILLUSION, 5) // The Pokemon generate an illusion if it's available .attr(IllusionPreSummonAbAttr, false) .attr(IllusionBreakAbAttr) @@ -7174,49 +7416,61 @@ export function initAbilities() { // Illusion is available again after a battle .conditionalAttr((pokemon) => pokemon.isAllowedInBattle(), IllusionPostBattleAbAttr, false) .uncopiable() - .bypassFaint(), - new Ability(AbilityId.IMPOSTER, 5) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.IMPOSTER, 5) .attr(PostSummonTransformAbAttr) .uncopiable() - .edgeCase(), // Should copy rage fist hit count, etc (see Transform edge case for full list) - new Ability(AbilityId.INFILTRATOR, 5) + .edgeCase() // Should copy rage fist hit count, etc (see Transform edge case for full list) + .build(), + new AbBuilder(AbilityId.INFILTRATOR, 5) .attr(InfiltratorAbAttr) - .partial(), // does not bypass Mist - new Ability(AbilityId.MUMMY, 5) + .partial() // does not bypass Mist + .build(), + new AbBuilder(AbilityId.MUMMY, 5) .attr(PostDefendAbilityGiveAbAttr, AbilityId.MUMMY) - .bypassFaint(), - new Ability(AbilityId.MOXIE, 5) - .attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1), - new Ability(AbilityId.JUSTIFIED, 5) - .attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => user.getMoveType(move) === PokemonType.DARK && move.category !== MoveCategory.STATUS, Stat.ATK, 1), - new Ability(AbilityId.RATTLED, 5) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.MOXIE, 5) + .attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1) + .build(), + new AbBuilder(AbilityId.JUSTIFIED, 5) + .attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => user.getMoveType(move) === PokemonType.DARK && move.category !== MoveCategory.STATUS, Stat.ATK, 1) + .build(), + new AbBuilder(AbilityId.RATTLED, 5) .attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => { const moveType = user.getMoveType(move); return move.category !== MoveCategory.STATUS && (moveType === PokemonType.DARK || moveType === PokemonType.BUG || moveType === PokemonType.GHOST); }, Stat.SPD, 1) - .attr(PostIntimidateStatStageChangeAbAttr, [ Stat.SPD ], 1), - new Ability(AbilityId.MAGIC_BOUNCE, 5) + .attr(PostIntimidateStatStageChangeAbAttr, [ Stat.SPD ], 1) + .build(), + new AbBuilder(AbilityId.MAGIC_BOUNCE, 5) .attr(ReflectStatusMoveAbAttr) .ignorable() // Interactions with stomping tantrum, instruct, encore, and probably other moves that // rely on move history - .edgeCase(), - new Ability(AbilityId.SAP_SIPPER, 5) + .edgeCase() + .build(), + new AbBuilder(AbilityId.SAP_SIPPER, 5) .attr(TypeImmunityStatStageChangeAbAttr, PokemonType.GRASS, Stat.ATK, 1) - .ignorable(), - new Ability(AbilityId.PRANKSTER, 5) - .attr(ChangeMovePriorityAbAttr, (_pokemon, move: Move) => move.category === MoveCategory.STATUS, 1), - new Ability(AbilityId.SAND_FORCE, 5) + .ignorable() + .build(), + new AbBuilder(AbilityId.PRANKSTER, 5) + .attr(ChangeMovePriorityAbAttr, (_pokemon, move: Move) => move.category === MoveCategory.STATUS, 1) + .build(), + new AbBuilder(AbilityId.SAND_FORCE, 5) .attr(MoveTypePowerBoostAbAttr, PokemonType.ROCK, 1.3) .attr(MoveTypePowerBoostAbAttr, PokemonType.GROUND, 1.3) .attr(MoveTypePowerBoostAbAttr, PokemonType.STEEL, 1.3) .attr(BlockWeatherDamageAttr, WeatherType.SANDSTORM) - .condition(getWeatherCondition(WeatherType.SANDSTORM)), - new Ability(AbilityId.IRON_BARBS, 5) + .condition(getWeatherCondition(WeatherType.SANDSTORM)) + .build(), + new AbBuilder(AbilityId.IRON_BARBS, 5) .attr(PostDefendContactDamageAbAttr, 8) - .bypassFaint(), - new Ability(AbilityId.ZEN_MODE, 5) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.ZEN_MODE, 5) .attr(PostBattleInitFormChangeAbAttr, () => 0) .attr(PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 ? 1 : 0) .attr(PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 ? 1 : 0) @@ -7224,20 +7478,25 @@ export function initAbilities() { .uncopiable() .unreplaceable() .unsuppressable() - .bypassFaint(), - new Ability(AbilityId.VICTORY_STAR, 5) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.VICTORY_STAR, 5) .attr(StatMultiplierAbAttr, Stat.ACC, 1.1) - .attr(AllyStatMultiplierAbAttr, Stat.ACC, 1.1, false), - new Ability(AbilityId.TURBOBLAZE, 5) + .attr(AllyStatMultiplierAbAttr, Stat.ACC, 1.1, false) + .build(), + new AbBuilder(AbilityId.TURBOBLAZE, 5) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTurboblaze", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) - .attr(MoveAbilityBypassAbAttr), - new Ability(AbilityId.TERAVOLT, 5) + .attr(MoveAbilityBypassAbAttr) + .build(), + new AbBuilder(AbilityId.TERAVOLT, 5) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) - .attr(MoveAbilityBypassAbAttr), - new Ability(AbilityId.AROMA_VEIL, 6) + .attr(MoveAbilityBypassAbAttr) + .build(), + new AbBuilder(AbilityId.AROMA_VEIL, 6) .attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ]) - .ignorable(), - new Ability(AbilityId.FLOWER_VEIL, 6) + .ignorable() + .build(), + new AbBuilder(AbilityId.FLOWER_VEIL, 6) .attr(ConditionalUserFieldStatusEffectImmunityAbAttr, (target: Pokemon, source: Pokemon | null) => { return source ? target.getTypes().includes(PokemonType.GRASS) && target.id !== source.id : false; }) @@ -7250,98 +7509,129 @@ export function initAbilities() { .attr(ConditionalUserFieldProtectStatAbAttr, (target: Pokemon) => { return target.getTypes().includes(PokemonType.GRASS); }) - .ignorable(), - new Ability(AbilityId.CHEEK_POUCH, 6) - .attr(HealFromBerryUseAbAttr, 1 / 3), - new Ability(AbilityId.PROTEAN, 6) + .ignorable() + .build(), + new AbBuilder(AbilityId.CHEEK_POUCH, 6) + .attr(HealFromBerryUseAbAttr, 1 / 3) + .build(), + new AbBuilder(AbilityId.PROTEAN, 6) .attr(PokemonTypeChangeAbAttr) // .condition((p) => !p.summonData.abilitiesApplied.includes(AbilityId.PROTEAN)) //Gen 9 Implementation // TODO: needs testing on interaction with weather blockage - .edgeCase(), - new Ability(AbilityId.FUR_COAT, 6) + .edgeCase() + .build(), + new AbBuilder(AbilityId.FUR_COAT, 6) .attr(ReceivedMoveDamageMultiplierAbAttr, (_target, _user, move) => move.category === MoveCategory.PHYSICAL, 0.5) - .ignorable(), - new Ability(AbilityId.MAGICIAN, 6) - .attr(PostAttackStealHeldItemAbAttr), - new Ability(AbilityId.BULLETPROOF, 6) + .ignorable() + .build(), + new AbBuilder(AbilityId.MAGICIAN, 6) + .attr(PostAttackStealHeldItemAbAttr) + .build(), + new AbBuilder(AbilityId.BULLETPROOF, 6) .attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.hasFlag(MoveFlags.BALLBOMB_MOVE)) - .ignorable(), - new Ability(AbilityId.COMPETITIVE, 6) - .attr(PostStatStageChangeStatStageChangeAbAttr, (_target, _statsChanged, stages) => stages < 0, [ Stat.SPATK ], 2), - new Ability(AbilityId.STRONG_JAW, 6) - .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.BITING_MOVE), 1.5), - new Ability(AbilityId.REFRIGERATE, 6) - .attr(MoveTypeChangeAbAttr, PokemonType.ICE, 1.2, (_user, _target, move) => move.type === PokemonType.NORMAL), - new Ability(AbilityId.SWEET_VEIL, 6) + .ignorable() + .build(), + new AbBuilder(AbilityId.COMPETITIVE, 6) + .attr(PostStatStageChangeStatStageChangeAbAttr, (_target, _statsChanged, stages) => stages < 0, [ Stat.SPATK ], 2) + .build(), + new AbBuilder(AbilityId.STRONG_JAW, 6) + .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.BITING_MOVE), 1.5) + .build(), + new AbBuilder(AbilityId.REFRIGERATE, 6) + .attr(MoveTypeChangeAbAttr, PokemonType.ICE, 1.2, (_user, _target, move) => move.type === PokemonType.NORMAL) + .build(), + new AbBuilder(AbilityId.SWEET_VEIL, 6) .attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.SLEEP) .attr(PostSummonUserFieldRemoveStatusEffectAbAttr, StatusEffect.SLEEP) .attr(UserFieldBattlerTagImmunityAbAttr, BattlerTagType.DROWSY) .ignorable() - .partial(), // Mold Breaker ally should not be affected by Sweet Veil - new Ability(AbilityId.STANCE_CHANGE, 6) + .partial() // Mold Breaker ally should not be affected by Sweet Veil + .build(), + new AbBuilder(AbilityId.STANCE_CHANGE, 6) .attr(NoFusionAbilityAbAttr) .uncopiable() .unreplaceable() - .unsuppressable(), - new Ability(AbilityId.GALE_WINGS, 6) - .attr(ChangeMovePriorityAbAttr, (pokemon, move) => pokemon.isFullHp() && pokemon.getMoveType(move) === PokemonType.FLYING, 1), - new Ability(AbilityId.MEGA_LAUNCHER, 6) - .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.PULSE_MOVE), 1.5), - new Ability(AbilityId.GRASS_PELT, 6) + .unsuppressable() + .build(), + new AbBuilder(AbilityId.GALE_WINGS, 6) + .attr(ChangeMovePriorityAbAttr, (pokemon, move) => pokemon.isFullHp() && pokemon.getMoveType(move) === PokemonType.FLYING, 1) + .build(), + new AbBuilder(AbilityId.MEGA_LAUNCHER, 6) + .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.PULSE_MOVE), 1.5) + .build(), + new AbBuilder(AbilityId.GRASS_PELT, 6) .conditionalAttr(getTerrainCondition(TerrainType.GRASSY), StatMultiplierAbAttr, Stat.DEF, 1.5) - .ignorable(), - new Ability(AbilityId.SYMBIOSIS, 6) - .unimplemented(), - new Ability(AbilityId.TOUGH_CLAWS, 6) - .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), 1.3), - new Ability(AbilityId.PIXILATE, 6) - .attr(MoveTypeChangeAbAttr, PokemonType.FAIRY, 1.2, (_user, _target, move) => move.type === PokemonType.NORMAL), - new Ability(AbilityId.GOOEY, 6) - .attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), Stat.SPD, -1, false), - new Ability(AbilityId.AERILATE, 6) - .attr(MoveTypeChangeAbAttr, PokemonType.FLYING, 1.2, (_user, _target, move) => move.type === PokemonType.NORMAL), - new Ability(AbilityId.PARENTAL_BOND, 6) - .attr(AddSecondStrikeAbAttr, 0.25), - new Ability(AbilityId.DARK_AURA, 6) + .ignorable() + .build(), + new AbBuilder(AbilityId.SYMBIOSIS, 6) + .unimplemented() + .build(), + new AbBuilder(AbilityId.TOUGH_CLAWS, 6) + .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), 1.3) + .build(), + new AbBuilder(AbilityId.PIXILATE, 6) + .attr(MoveTypeChangeAbAttr, PokemonType.FAIRY, 1.2, (_user, _target, move) => move.type === PokemonType.NORMAL) + .build(), + new AbBuilder(AbilityId.GOOEY, 6) + .attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), Stat.SPD, -1, false) + .build(), + new AbBuilder(AbilityId.AERILATE, 6) + .attr(MoveTypeChangeAbAttr, PokemonType.FLYING, 1.2, (_user, _target, move) => move.type === PokemonType.NORMAL) + .build(), + new AbBuilder(AbilityId.PARENTAL_BOND, 6) + .attr(AddSecondStrikeAbAttr, 0.25) + .build(), + new AbBuilder(AbilityId.DARK_AURA, 6) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonDarkAura", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) - .attr(FieldMoveTypePowerBoostAbAttr, PokemonType.DARK, 4 / 3), - new Ability(AbilityId.FAIRY_AURA, 6) + .attr(FieldMoveTypePowerBoostAbAttr, PokemonType.DARK, 4 / 3) + .build(), + new AbBuilder(AbilityId.FAIRY_AURA, 6) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonFairyAura", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) - .attr(FieldMoveTypePowerBoostAbAttr, PokemonType.FAIRY, 4 / 3), - new Ability(AbilityId.AURA_BREAK, 6) + .attr(FieldMoveTypePowerBoostAbAttr, PokemonType.FAIRY, 4 / 3) + .build(), + new AbBuilder(AbilityId.AURA_BREAK, 6) .ignorable() .conditionalAttr(_pokemon => globalScene.getField(true).some(p => p.hasAbility(AbilityId.DARK_AURA)), FieldMoveTypePowerBoostAbAttr, PokemonType.DARK, 9 / 16) .conditionalAttr(_pokemon => globalScene.getField(true).some(p => p.hasAbility(AbilityId.FAIRY_AURA)), FieldMoveTypePowerBoostAbAttr, PokemonType.FAIRY, 9 / 16) .conditionalAttr(_pokemon => globalScene.getField(true).some(p => p.hasAbility(AbilityId.DARK_AURA) || p.hasAbility(AbilityId.FAIRY_AURA)), - PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAuraBreak", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })), - new Ability(AbilityId.PRIMORDIAL_SEA, 6) + PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAuraBreak", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) + .build(), + new AbBuilder(AbilityId.PRIMORDIAL_SEA, 6) .attr(PostSummonWeatherChangeAbAttr, WeatherType.HEAVY_RAIN) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.HEAVY_RAIN) .attr(PreLeaveFieldClearWeatherAbAttr) - .bypassFaint(), - new Ability(AbilityId.DESOLATE_LAND, 6) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.DESOLATE_LAND, 6) .attr(PostSummonWeatherChangeAbAttr, WeatherType.HARSH_SUN) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.HARSH_SUN) .attr(PreLeaveFieldClearWeatherAbAttr) - .bypassFaint(), - new Ability(AbilityId.DELTA_STREAM, 6) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.DELTA_STREAM, 6) .attr(PostSummonWeatherChangeAbAttr, WeatherType.STRONG_WINDS) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.STRONG_WINDS) .attr(PreLeaveFieldClearWeatherAbAttr) - .bypassFaint(), - new Ability(AbilityId.STAMINA, 7) - .attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1), - new Ability(AbilityId.WIMP_OUT, 7) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.STAMINA, 7) + .attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1) + .build(), + new AbBuilder(AbilityId.WIMP_OUT, 7) .attr(PostDamageForceSwitchAbAttr) - .edgeCase(), // Should not trigger when hurting itself in confusion, causes Fake Out to fail turn 1 and succeed turn 2 if pokemon is switched out before battle start via playing in Switch Mode - new Ability(AbilityId.EMERGENCY_EXIT, 7) + .edgeCase() // Should not trigger when hurting itself in confusion, causes Fake Out to fail turn 1 and succeed turn 2 if pokemon is switched out before battle start via playing in Switch Mode + .build(), + new AbBuilder(AbilityId.EMERGENCY_EXIT, 7) .attr(PostDamageForceSwitchAbAttr) - .edgeCase(), // Should not trigger when hurting itself in confusion, causes Fake Out to fail turn 1 and succeed turn 2 if pokemon is switched out before battle start via playing in Switch Mode - new Ability(AbilityId.WATER_COMPACTION, 7) - .attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => user.getMoveType(move) === PokemonType.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2), - new Ability(AbilityId.MERCILESS, 7) - .attr(ConditionalCritAbAttr, (_user, target, _move) => target?.status?.effect === StatusEffect.TOXIC || target?.status?.effect === StatusEffect.POISON), - new Ability(AbilityId.SHIELDS_DOWN, 7, -1) + .edgeCase() // Should not trigger when hurting itself in confusion, causes Fake Out to fail turn 1 and succeed turn 2 if pokemon is switched out before battle start via playing in Switch Mode + .build(), + new AbBuilder(AbilityId.WATER_COMPACTION, 7) + .attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => user.getMoveType(move) === PokemonType.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2) + .build(), + new AbBuilder(AbilityId.MERCILESS, 7) + .attr(ConditionalCritAbAttr, (_user, target, _move) => target?.status?.effect === StatusEffect.TOXIC || target?.status?.effect === StatusEffect.POISON) + .build(), + new AbBuilder(AbilityId.SHIELDS_DOWN, 7, -1) // Change into Meteor Form on switch-in or turn end if HP >= 50%, // or Core Form if HP <= 50%. .attr(PostBattleInitFormChangeAbAttr, p => p.formIndex % 7) @@ -7355,34 +7645,45 @@ export function initAbilities() { .uncopiable() .unreplaceable() .unsuppressable() - .bypassFaint(), - new Ability(AbilityId.STAKEOUT, 7) - .attr(MovePowerBoostAbAttr, (_user, target, _move) => !!target?.turnData.switchedInThisTurn, 2), - new Ability(AbilityId.WATER_BUBBLE, 7) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.STAKEOUT, 7) + .attr(MovePowerBoostAbAttr, (_user, target, _move) => !!target?.turnData.switchedInThisTurn, 2) + .build(), + new AbBuilder(AbilityId.WATER_BUBBLE, 7) .attr(ReceivedTypeDamageMultiplierAbAttr, PokemonType.FIRE, 0.5) .attr(MoveTypePowerBoostAbAttr, PokemonType.WATER, 2) .attr(StatusEffectImmunityAbAttr, StatusEffect.BURN) .attr(PostSummonHealStatusAbAttr, StatusEffect.BURN) - .ignorable(), - new Ability(AbilityId.STEELWORKER, 7) - .attr(MoveTypePowerBoostAbAttr, PokemonType.STEEL), - new Ability(AbilityId.BERSERK, 7) + .ignorable() + .build(), + new AbBuilder(AbilityId.STEELWORKER, 7) + .attr(MoveTypePowerBoostAbAttr, PokemonType.STEEL) + .build(), + new AbBuilder(AbilityId.BERSERK, 7) .attr(PostDefendHpGatedStatStageChangeAbAttr, (_target, _user, move) => move.category !== MoveCategory.STATUS, 0.5, [ Stat.SPATK ], 1) - .condition(getSheerForceHitDisableAbCondition()), - new Ability(AbilityId.SLUSH_RUSH, 7) + .condition(getSheerForceHitDisableAbCondition()) + .build(), + new AbBuilder(AbilityId.SLUSH_RUSH, 7) .attr(StatMultiplierAbAttr, Stat.SPD, 2) - .condition(getWeatherCondition(WeatherType.HAIL, WeatherType.SNOW)), - new Ability(AbilityId.LONG_REACH, 7) - .attr(IgnoreContactAbAttr), - new Ability(AbilityId.LIQUID_VOICE, 7) - .attr(MoveTypeChangeAbAttr, PokemonType.WATER, 1, (_user, _target, move) => move.hasFlag(MoveFlags.SOUND_BASED)), - new Ability(AbilityId.TRIAGE, 7) - .attr(ChangeMovePriorityAbAttr, (_pokemon, move) => move.hasFlag(MoveFlags.TRIAGE_MOVE), 3), - new Ability(AbilityId.GALVANIZE, 7) - .attr(MoveTypeChangeAbAttr, PokemonType.ELECTRIC, 1.2, (_user, _target, move) => move.type === PokemonType.NORMAL), - new Ability(AbilityId.SURGE_SURFER, 7) - .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), StatMultiplierAbAttr, Stat.SPD, 2), - new Ability(AbilityId.SCHOOLING, 7, -1) + .condition(getWeatherCondition(WeatherType.HAIL, WeatherType.SNOW)) + .build(), + new AbBuilder(AbilityId.LONG_REACH, 7) + .attr(IgnoreContactAbAttr) + .build(), + new AbBuilder(AbilityId.LIQUID_VOICE, 7) + .attr(MoveTypeChangeAbAttr, PokemonType.WATER, 1, (_user, _target, move) => move.hasFlag(MoveFlags.SOUND_BASED)) + .build(), + new AbBuilder(AbilityId.TRIAGE, 7) + .attr(ChangeMovePriorityAbAttr, (_pokemon, move) => move.hasFlag(MoveFlags.TRIAGE_MOVE), 3) + .build(), + new AbBuilder(AbilityId.GALVANIZE, 7) + .attr(MoveTypeChangeAbAttr, PokemonType.ELECTRIC, 1.2, (_user, _target, move) => move.type === PokemonType.NORMAL) + .build(), + new AbBuilder(AbilityId.SURGE_SURFER, 7) + .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), StatMultiplierAbAttr, Stat.SPD, 2) + .build(), + new AbBuilder(AbilityId.SCHOOLING, 7, -1) .attr(PostBattleInitFormChangeAbAttr, () => 0) .attr(PostSummonFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1) .attr(PostTurnFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1) @@ -7390,8 +7691,9 @@ export function initAbilities() { .uncopiable() .unreplaceable() .unsuppressable() - .bypassFaint(), - new Ability(AbilityId.DISGUISE, 7) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.DISGUISE, 7) .attr(NoTransformAbilityAbAttr) .attr(NoFusionAbilityAbAttr) // Add BattlerTagType.DISGUISE if the pokemon is in its disguised form @@ -7406,8 +7708,9 @@ export function initAbilities() { .unreplaceable() .unsuppressable() .bypassFaint() - .ignorable(), - new Ability(AbilityId.BATTLE_BOND, 7) + .ignorable() + .build(), + new AbBuilder(AbilityId.BATTLE_BOND, 7) .attr(PostVictoryFormChangeAbAttr, () => 2) .attr(PostBattleInitFormChangeAbAttr, () => 1) .attr(PostFaintFormChangeAbAttr, () => 1) @@ -7415,8 +7718,9 @@ export function initAbilities() { .uncopiable() .unreplaceable() .unsuppressable() - .bypassFaint(), - new Ability(AbilityId.POWER_CONSTRUCT, 7) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.POWER_CONSTRUCT, 7) // Change to 10% complete or 50% complete on switchout/turn end if at <50% HP; // revert to 10% PC or 50% PC before a new battle starts .conditionalAttr(p => p.formIndex === 4 || p.formIndex === 5, PostBattleInitFormChangeAbAttr, p => p.formIndex - 2) @@ -7427,49 +7731,62 @@ export function initAbilities() { .uncopiable() .unreplaceable() .unsuppressable() - .bypassFaint(), - new Ability(AbilityId.CORROSION, 7) - .attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ PokemonType.STEEL, PokemonType.POISON ]), - new Ability(AbilityId.COMATOSE, 7) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.CORROSION, 7) + .attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ PokemonType.STEEL, PokemonType.POISON ]) + .build(), + new AbBuilder(AbilityId.COMATOSE, 7) .attr(StatusEffectImmunityAbAttr, ...getNonVolatileStatusEffects()) .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY) .uncopiable() .unreplaceable() - .unsuppressable(), - new Ability(AbilityId.QUEENLY_MAJESTY, 7) + .unsuppressable() + .build(), + new AbBuilder(AbilityId.QUEENLY_MAJESTY, 7) .attr(FieldPriorityMoveImmunityAbAttr) - .ignorable(), - new Ability(AbilityId.INNARDS_OUT, 7) + .ignorable() + .build(), + new AbBuilder(AbilityId.INNARDS_OUT, 7) .attr(PostFaintHPDamageAbAttr) - .bypassFaint(), - new Ability(AbilityId.DANCER, 7) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.DANCER, 7) .attr(PostDancingMoveAbAttr) /* Incorrect interations with: * Petal Dance (should not lock in or count down timer; currently does both) * Flinches (due to tag being removed earlier) * Failed/protected moves (should not trigger if original move is protected against) */ - .edgeCase(), - new Ability(AbilityId.BATTERY, 7) - .attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL ], 1.3), - new Ability(AbilityId.FLUFFY, 7) + .edgeCase() + .build(), + new AbBuilder(AbilityId.BATTERY, 7) + .attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL ], 1.3) + .build(), + new AbBuilder(AbilityId.FLUFFY, 7) .attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user, target}), 0.5) .attr(ReceivedMoveDamageMultiplierAbAttr, (_target, user, move) => user.getMoveType(move) === PokemonType.FIRE, 2) - .ignorable(), - new Ability(AbilityId.DAZZLING, 7) + .ignorable() + .build(), + new AbBuilder(AbilityId.DAZZLING, 7) .attr(FieldPriorityMoveImmunityAbAttr) - .ignorable(), - new Ability(AbilityId.SOUL_HEART, 7) - .attr(PostKnockOutStatStageChangeAbAttr, Stat.SPATK, 1), - new Ability(AbilityId.TANGLING_HAIR, 7) - .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user, target}), Stat.SPD, -1, false), - new Ability(AbilityId.RECEIVER, 7) + .ignorable() + .build(), + new AbBuilder(AbilityId.SOUL_HEART, 7) + .attr(PostKnockOutStatStageChangeAbAttr, Stat.SPATK, 1) + .build(), + new AbBuilder(AbilityId.TANGLING_HAIR, 7) + .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user, target}), Stat.SPD, -1, false) + .build(), + new AbBuilder(AbilityId.RECEIVER, 7) .attr(CopyFaintedAllyAbilityAbAttr) - .uncopiable(), - new Ability(AbilityId.POWER_OF_ALCHEMY, 7) + .uncopiable() + .build(), + new AbBuilder(AbilityId.POWER_OF_ALCHEMY, 7) .attr(CopyFaintedAllyAbilityAbAttr) - .uncopiable(), - new Ability(AbilityId.BEAST_BOOST, 7) + .uncopiable() + .build(), + new AbBuilder(AbilityId.BEAST_BOOST, 7) .attr(PostVictoryStatStageChangeAbAttr, p => { let highestStat: EffectiveStat; let highestValue = 0; @@ -7481,86 +7798,110 @@ export function initAbilities() { } } return highestStat!; - }, 1), - new Ability(AbilityId.RKS_SYSTEM, 7) + }, 1) + .build(), + new AbBuilder(AbilityId.RKS_SYSTEM, 7) .attr(NoFusionAbilityAbAttr) .uncopiable() .unreplaceable() - .unsuppressable(), - new Ability(AbilityId.ELECTRIC_SURGE, 7) + .unsuppressable() + .build(), + new AbBuilder(AbilityId.ELECTRIC_SURGE, 7) .attr(PostSummonTerrainChangeAbAttr, TerrainType.ELECTRIC) - .attr(PostBiomeChangeTerrainChangeAbAttr, TerrainType.ELECTRIC), - new Ability(AbilityId.PSYCHIC_SURGE, 7) + .attr(PostBiomeChangeTerrainChangeAbAttr, TerrainType.ELECTRIC) + .build(), + new AbBuilder(AbilityId.PSYCHIC_SURGE, 7) .attr(PostSummonTerrainChangeAbAttr, TerrainType.PSYCHIC) - .attr(PostBiomeChangeTerrainChangeAbAttr, TerrainType.PSYCHIC), - new Ability(AbilityId.MISTY_SURGE, 7) + .attr(PostBiomeChangeTerrainChangeAbAttr, TerrainType.PSYCHIC) + .build(), + new AbBuilder(AbilityId.MISTY_SURGE, 7) .attr(PostSummonTerrainChangeAbAttr, TerrainType.MISTY) - .attr(PostBiomeChangeTerrainChangeAbAttr, TerrainType.MISTY), - new Ability(AbilityId.GRASSY_SURGE, 7) + .attr(PostBiomeChangeTerrainChangeAbAttr, TerrainType.MISTY) + .build(), + new AbBuilder(AbilityId.GRASSY_SURGE, 7) .attr(PostSummonTerrainChangeAbAttr, TerrainType.GRASSY) - .attr(PostBiomeChangeTerrainChangeAbAttr, TerrainType.GRASSY), - new Ability(AbilityId.FULL_METAL_BODY, 7) - .attr(ProtectStatAbAttr), - new Ability(AbilityId.SHADOW_SHIELD, 7) - .attr(ReceivedMoveDamageMultiplierAbAttr, (target, _user, _move) => target.isFullHp(), 0.5), - new Ability(AbilityId.PRISM_ARMOR, 7) - .attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getMoveEffectiveness(user, move) >= 2, 0.75), - new Ability(AbilityId.NEUROFORCE, 7) - .attr(MovePowerBoostAbAttr, (user, target, move) => (target?.getMoveEffectiveness(user!, move) ?? 1) >= 2, 1.25), - new Ability(AbilityId.INTREPID_SWORD, 8) - .attr(PostSummonStatStageChangeAbAttr, [ Stat.ATK ], 1, true), - new Ability(AbilityId.DAUNTLESS_SHIELD, 8) - .attr(PostSummonStatStageChangeAbAttr, [ Stat.DEF ], 1, true), - new Ability(AbilityId.LIBERO, 8) + .attr(PostBiomeChangeTerrainChangeAbAttr, TerrainType.GRASSY) + .build(), + new AbBuilder(AbilityId.FULL_METAL_BODY, 7) + .attr(ProtectStatAbAttr) + .build(), + new AbBuilder(AbilityId.SHADOW_SHIELD, 7) + .attr(ReceivedMoveDamageMultiplierAbAttr, (target, _user, _move) => target.isFullHp(), 0.5) + .build(), + new AbBuilder(AbilityId.PRISM_ARMOR, 7) + .attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getMoveEffectiveness(user, move) >= 2, 0.75) + .build(), + new AbBuilder(AbilityId.NEUROFORCE, 7) + .attr(MovePowerBoostAbAttr, (user, target, move) => (target?.getMoveEffectiveness(user!, move) ?? 1) >= 2, 1.25) + .build(), + new AbBuilder(AbilityId.INTREPID_SWORD, 8) + .attr(PostSummonStatStageChangeAbAttr, [ Stat.ATK ], 1, true) + .build(), + new AbBuilder(AbilityId.DAUNTLESS_SHIELD, 8) + .attr(PostSummonStatStageChangeAbAttr, [ Stat.DEF ], 1, true) + .build(), + new AbBuilder(AbilityId.LIBERO, 8) .attr(PokemonTypeChangeAbAttr) //.condition((p) => !p.summonData.abilitiesApplied.includes(AbilityId.LIBERO)), //Gen 9 Implementation // TODO: needs testing on interaction with weather blockage - .edgeCase(), - new Ability(AbilityId.BALL_FETCH, 8) + .edgeCase() + .build(), + new AbBuilder(AbilityId.BALL_FETCH, 8) .attr(FetchBallAbAttr) - .condition(getOncePerBattleCondition(AbilityId.BALL_FETCH)), - new Ability(AbilityId.COTTON_DOWN, 8) + .condition(getOncePerBattleCondition(AbilityId.BALL_FETCH)) + .build(), + new AbBuilder(AbilityId.COTTON_DOWN, 8) .attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => move.category !== MoveCategory.STATUS, Stat.SPD, -1, false, true) - .bypassFaint(), - new Ability(AbilityId.PROPELLER_TAIL, 8) - .attr(BlockRedirectAbAttr), - new Ability(AbilityId.MIRROR_ARMOR, 8) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.PROPELLER_TAIL, 8) + .attr(BlockRedirectAbAttr) + .build(), + new AbBuilder(AbilityId.MIRROR_ARMOR, 8) .attr(ReflectStatStageChangeAbAttr) - .ignorable(), + .ignorable() + .build(), /** * Right now, the logic is attached to Surf and Dive moves. Ideally, the post-defend/hit should be an * ability attribute but the current implementation of move effects for BattlerTag does not support this- in the case * where Cramorant is fainted. * @see {@linkcode GulpMissileTagAttr} and {@linkcode GulpMissileTag} for Gulp Missile implementation */ - new Ability(AbilityId.GULP_MISSILE, 8) + new AbBuilder(AbilityId.GULP_MISSILE, 8) .attr(NoTransformAbilityAbAttr) .attr(NoFusionAbilityAbAttr) .unsuppressable() .uncopiable() .unreplaceable() - .bypassFaint(), - new Ability(AbilityId.STALWART, 8) - .attr(BlockRedirectAbAttr), - new Ability(AbilityId.STEAM_ENGINE, 8) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.STALWART, 8) + .attr(BlockRedirectAbAttr) + .build(), + new AbBuilder(AbilityId.STEAM_ENGINE, 8) .attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => { const moveType = user.getMoveType(move); return move.category !== MoveCategory.STATUS && (moveType === PokemonType.FIRE || moveType === PokemonType.WATER); - }, Stat.SPD, 6), - new Ability(AbilityId.PUNK_ROCK, 8) + }, Stat.SPD, 6) + .build(), + new AbBuilder(AbilityId.PUNK_ROCK, 8) .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.SOUND_BASED), 1.3) .attr(ReceivedMoveDamageMultiplierAbAttr, (_target, _user, move) => move.hasFlag(MoveFlags.SOUND_BASED), 0.5) - .ignorable(), - new Ability(AbilityId.SAND_SPIT, 8) + .ignorable() + .build(), + new AbBuilder(AbilityId.SAND_SPIT, 8) .attr(PostDefendWeatherChangeAbAttr, WeatherType.SANDSTORM, (_target, _user, move) => move.category !== MoveCategory.STATUS) - .bypassFaint(), - new Ability(AbilityId.ICE_SCALES, 8) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.ICE_SCALES, 8) .attr(ReceivedMoveDamageMultiplierAbAttr, (_target, _user, move) => move.category === MoveCategory.SPECIAL, 0.5) - .ignorable(), - new Ability(AbilityId.RIPEN, 8) - .attr(DoubleBerryEffectAbAttr), - new Ability(AbilityId.ICE_FACE, 8, -2) + .ignorable() + .build(), + new AbBuilder(AbilityId.RIPEN, 8) + .attr(DoubleBerryEffectAbAttr) + .build(), + new AbBuilder(AbilityId.ICE_FACE, 8, -2) .attr(NoTransformAbilityAbAttr) .attr(NoFusionAbilityAbAttr) // Add BattlerTagType.ICE_FACE if the pokemon is in ice face form @@ -7577,107 +7918,137 @@ export function initAbilities() { .unreplaceable() .unsuppressable() .bypassFaint() - .ignorable(), - new Ability(AbilityId.POWER_SPOT, 8) - .attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL, MoveCategory.PHYSICAL ], 1.3), - new Ability(AbilityId.MIMICRY, 8, -1) - .attr(TerrainEventTypeChangeAbAttr), - new Ability(AbilityId.SCREEN_CLEANER, 8) - .attr(PostSummonRemoveArenaTagAbAttr, [ ArenaTagType.AURORA_VEIL, ArenaTagType.LIGHT_SCREEN, ArenaTagType.REFLECT ]), - new Ability(AbilityId.STEELY_SPIRIT, 8) - .attr(UserFieldMoveTypePowerBoostAbAttr, PokemonType.STEEL), - new Ability(AbilityId.PERISH_BODY, 8) + .ignorable() + .build(), + new AbBuilder(AbilityId.POWER_SPOT, 8) + .attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL, MoveCategory.PHYSICAL ], 1.3) + .build(), + new AbBuilder(AbilityId.MIMICRY, 8, -1) + .attr(TerrainEventTypeChangeAbAttr) + .build(), + new AbBuilder(AbilityId.SCREEN_CLEANER, 8) + .attr(PostSummonRemoveArenaTagAbAttr, [ ArenaTagType.AURORA_VEIL, ArenaTagType.LIGHT_SCREEN, ArenaTagType.REFLECT ]) + .build(), + new AbBuilder(AbilityId.STEELY_SPIRIT, 8) + .attr(UserFieldMoveTypePowerBoostAbAttr, PokemonType.STEEL) + .build(), + new AbBuilder(AbilityId.PERISH_BODY, 8) .attr(PostDefendPerishSongAbAttr, 4) - .bypassFaint(), - new Ability(AbilityId.WANDERING_SPIRIT, 8) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.WANDERING_SPIRIT, 8) .attr(PostDefendAbilitySwapAbAttr) .bypassFaint() - .edgeCase(), // interacts incorrectly with rock head. It's meant to switch abilities before recoil would apply so that a pokemon with rock head would lose rock head first and still take the recoil - new Ability(AbilityId.GORILLA_TACTICS, 8) + .edgeCase() // interacts incorrectly with rock head. It's meant to switch abilities before recoil would apply so that a pokemon with rock head would lose rock head first and still take the recoil + .build(), + new AbBuilder(AbilityId.GORILLA_TACTICS, 8) .attr(GorillaTacticsAbAttr) // TODO: Verify whether Gorilla Tactics increases struggle's power or not - .edgeCase(), - new Ability(AbilityId.NEUTRALIZING_GAS, 8, 2) + .edgeCase() + .build(), + new AbBuilder(AbilityId.NEUTRALIZING_GAS, 8, 2) .attr(PostSummonAddArenaTagAbAttr, true, ArenaTagType.NEUTRALIZING_GAS, 0) .attr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr) .uncopiable() .attr(NoTransformAbilityAbAttr) - .bypassFaint(), - new Ability(AbilityId.PASTEL_VEIL, 8) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.PASTEL_VEIL, 8) .attr(PostSummonUserFieldRemoveStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) .attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) - .ignorable(), - new Ability(AbilityId.HUNGER_SWITCH, 8) + .ignorable() + .build(), + new AbBuilder(AbilityId.HUNGER_SWITCH, 8) .attr(PostTurnFormChangeAbAttr, p => p.getFormKey() ? 0 : 1) .attr(PostTurnFormChangeAbAttr, p => p.getFormKey() ? 1 : 0) .attr(NoTransformAbilityAbAttr) .attr(NoFusionAbilityAbAttr) .condition((pokemon) => !pokemon.isTerastallized) .uncopiable() - .unreplaceable(), - new Ability(AbilityId.QUICK_DRAW, 8) - .attr(BypassSpeedChanceAbAttr, 30), - new Ability(AbilityId.UNSEEN_FIST, 8) - .attr(IgnoreProtectOnContactAbAttr), - new Ability(AbilityId.CURIOUS_MEDICINE, 8) - .attr(PostSummonClearAllyStatStagesAbAttr), - new Ability(AbilityId.TRANSISTOR, 8) - .attr(MoveTypePowerBoostAbAttr, PokemonType.ELECTRIC, 1.3), - new Ability(AbilityId.DRAGONS_MAW, 8) - .attr(MoveTypePowerBoostAbAttr, PokemonType.DRAGON), - new Ability(AbilityId.CHILLING_NEIGH, 8) - .attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1), - new Ability(AbilityId.GRIM_NEIGH, 8) - .attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1), - new Ability(AbilityId.AS_ONE_GLASTRIER, 8, 1) + .unreplaceable() + .build(), + new AbBuilder(AbilityId.QUICK_DRAW, 8) + .attr(BypassSpeedChanceAbAttr, 30) + .build(), + new AbBuilder(AbilityId.UNSEEN_FIST, 8) + .attr(IgnoreProtectOnContactAbAttr) + .build(), + new AbBuilder(AbilityId.CURIOUS_MEDICINE, 8) + .attr(PostSummonClearAllyStatStagesAbAttr) + .build(), + new AbBuilder(AbilityId.TRANSISTOR, 8) + .attr(MoveTypePowerBoostAbAttr, PokemonType.ELECTRIC, 1.3) + .build(), + new AbBuilder(AbilityId.DRAGONS_MAW, 8) + .attr(MoveTypePowerBoostAbAttr, PokemonType.DRAGON) + .build(), + new AbBuilder(AbilityId.CHILLING_NEIGH, 8) + .attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1) + .build(), + new AbBuilder(AbilityId.GRIM_NEIGH, 8) + .attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1) + .build(), + new AbBuilder(AbilityId.AS_ONE_GLASTRIER, 8, 1) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneGlastrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(PreventBerryUseAbAttr) .attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1) .uncopiable() .unreplaceable() - .unsuppressable(), - new Ability(AbilityId.AS_ONE_SPECTRIER, 8, 1) + .unsuppressable() + .build(), + new AbBuilder(AbilityId.AS_ONE_SPECTRIER, 8, 1) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneSpectrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(PreventBerryUseAbAttr) .attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1) .uncopiable() .unreplaceable() - .unsuppressable(), - new Ability(AbilityId.LINGERING_AROMA, 9) + .unsuppressable() + .build(), + new AbBuilder(AbilityId.LINGERING_AROMA, 9) .attr(PostDefendAbilityGiveAbAttr, AbilityId.LINGERING_AROMA) - .bypassFaint(), - new Ability(AbilityId.SEED_SOWER, 9) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.SEED_SOWER, 9) .attr(PostDefendTerrainChangeAbAttr, TerrainType.GRASSY) - .bypassFaint(), - new Ability(AbilityId.THERMAL_EXCHANGE, 9) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.THERMAL_EXCHANGE, 9) .attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => user.getMoveType(move) === PokemonType.FIRE && move.category !== MoveCategory.STATUS, Stat.ATK, 1) .attr(StatusEffectImmunityAbAttr, StatusEffect.BURN) .attr(PostSummonHealStatusAbAttr, StatusEffect.BURN) - .ignorable(), - new Ability(AbilityId.ANGER_SHELL, 9) + .ignorable() + .build(), + new AbBuilder(AbilityId.ANGER_SHELL, 9) .attr(PostDefendHpGatedStatStageChangeAbAttr, (_target, _user, move) => move.category !== MoveCategory.STATUS, 0.5, [ Stat.ATK, Stat.SPATK, Stat.SPD ], 1) .attr(PostDefendHpGatedStatStageChangeAbAttr, (_target, _user, move) => move.category !== MoveCategory.STATUS, 0.5, [ Stat.DEF, Stat.SPDEF ], -1) - .condition(getSheerForceHitDisableAbCondition()), - new Ability(AbilityId.PURIFYING_SALT, 9) + .condition(getSheerForceHitDisableAbCondition()) + .build(), + new AbBuilder(AbilityId.PURIFYING_SALT, 9) .attr(StatusEffectImmunityAbAttr) .attr(ReceivedTypeDamageMultiplierAbAttr, PokemonType.GHOST, 0.5) - .ignorable(), - new Ability(AbilityId.WELL_BAKED_BODY, 9) + .ignorable() + .build(), + new AbBuilder(AbilityId.WELL_BAKED_BODY, 9) .attr(TypeImmunityStatStageChangeAbAttr, PokemonType.FIRE, Stat.DEF, 2) - .ignorable(), - new Ability(AbilityId.WIND_RIDER, 9) + .ignorable() + .build(), + new AbBuilder(AbilityId.WIND_RIDER, 9) .attr(MoveImmunityStatStageChangeAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.hasFlag(MoveFlags.WIND_MOVE) && move.category !== MoveCategory.STATUS, Stat.ATK, 1) .attr(PostSummonStatStageChangeOnArenaAbAttr, ArenaTagType.TAILWIND) - .ignorable(), - new Ability(AbilityId.GUARD_DOG, 9) + .ignorable() + .build(), + new AbBuilder(AbilityId.GUARD_DOG, 9) .attr(PostIntimidateStatStageChangeAbAttr, [ Stat.ATK ], 1, true) .attr(ForceSwitchOutImmunityAbAttr) - .ignorable(), - new Ability(AbilityId.ROCKY_PAYLOAD, 9) - .attr(MoveTypePowerBoostAbAttr, PokemonType.ROCK), - new Ability(AbilityId.WIND_POWER, 9) - .attr(PostDefendApplyBattlerTagAbAttr, (_target, _user, move) => move.hasFlag(MoveFlags.WIND_MOVE), BattlerTagType.CHARGED), - new Ability(AbilityId.ZERO_TO_HERO, 9) + .ignorable() + .build(), + new AbBuilder(AbilityId.ROCKY_PAYLOAD, 9) + .attr(MoveTypePowerBoostAbAttr, PokemonType.ROCK) + .build(), + new AbBuilder(AbilityId.WIND_POWER, 9) + .attr(PostDefendApplyBattlerTagAbAttr, (_target, _user, move) => move.hasFlag(MoveFlags.WIND_MOVE), BattlerTagType.CHARGED) + .build(), + new AbBuilder(AbilityId.ZERO_TO_HERO, 9) .uncopiable() .unreplaceable() .unsuppressable() @@ -7685,136 +8056,168 @@ export function initAbilities() { .attr(NoFusionAbilityAbAttr) .attr(PostBattleInitFormChangeAbAttr, () => 0) .attr(PreSwitchOutFormChangeAbAttr, (pokemon) => !pokemon.isFainted() ? 1 : pokemon.formIndex) - .bypassFaint(), - new Ability(AbilityId.COMMANDER, 9) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.COMMANDER, 9) .attr(CommanderAbAttr) .attr(DoubleBattleChanceAbAttr) .uncopiable() .unreplaceable() - .edgeCase(), // Encore, Frenzy, and other non-`TURN_END` tags don't lapse correctly on the commanding Pokemon. - new Ability(AbilityId.ELECTROMORPHOSIS, 9) - .attr(PostDefendApplyBattlerTagAbAttr, (_target, _user, move) => move.category !== MoveCategory.STATUS, BattlerTagType.CHARGED), - new Ability(AbilityId.PROTOSYNTHESIS, 9, -2) + .edgeCase() // Encore, Frenzy, and other non-`TURN_END` tags don't lapse correctly on the commanding Pokemon. + .build(), + new AbBuilder(AbilityId.ELECTROMORPHOSIS, 9) + .attr(PostDefendApplyBattlerTagAbAttr, (_target, _user, move) => move.category !== MoveCategory.STATUS, BattlerTagType.CHARGED) + .build(), + new AbBuilder(AbilityId.PROTOSYNTHESIS, 9, -2) .conditionalAttr(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN), PostSummonAddBattlerTagAbAttr, BattlerTagType.PROTOSYNTHESIS, 0, true) .attr(PostWeatherChangeAddBattlerTagAttr, BattlerTagType.PROTOSYNTHESIS, 0, WeatherType.SUNNY, WeatherType.HARSH_SUN) .uncopiable() - .attr(NoTransformAbilityAbAttr), - new Ability(AbilityId.QUARK_DRIVE, 9, -2) + .attr(NoTransformAbilityAbAttr) + .build(), + new AbBuilder(AbilityId.QUARK_DRIVE, 9, -2) .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), PostSummonAddBattlerTagAbAttr, BattlerTagType.QUARK_DRIVE, 0, true) .attr(PostTerrainChangeAddBattlerTagAttr, BattlerTagType.QUARK_DRIVE, 0, TerrainType.ELECTRIC) .uncopiable() - .attr(NoTransformAbilityAbAttr), - new Ability(AbilityId.GOOD_AS_GOLD, 9) + .attr(NoTransformAbilityAbAttr) + .build(), + new AbBuilder(AbilityId.GOOD_AS_GOLD, 9) .attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.category === MoveCategory.STATUS && ![ MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES, MoveTarget.USER_SIDE ].includes(move.moveTarget) ) .edgeCase() // Heal Bell should not cure the status of a Pokemon with Good As Gold - .ignorable(), - new Ability(AbilityId.VESSEL_OF_RUIN, 9) + .ignorable() + .build(), + new AbBuilder(AbilityId.VESSEL_OF_RUIN, 9) .attr(FieldMultiplyStatAbAttr, Stat.SPATK, 0.75) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonVesselOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPATK)) })) - .ignorable(), - new Ability(AbilityId.SWORD_OF_RUIN, 9) + .ignorable() + .build(), + new AbBuilder(AbilityId.SWORD_OF_RUIN, 9) .attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75) - .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })), - new Ability(AbilityId.TABLETS_OF_RUIN, 9) + .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })) + .build(), + new AbBuilder(AbilityId.TABLETS_OF_RUIN, 9) .attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) })) - .ignorable(), - new Ability(AbilityId.BEADS_OF_RUIN, 9) + .ignorable() + .build(), + new AbBuilder(AbilityId.BEADS_OF_RUIN, 9) .attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75) - .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })), - new Ability(AbilityId.ORICHALCUM_PULSE, 9) + .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })) + .build(), + new AbBuilder(AbilityId.ORICHALCUM_PULSE, 9) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY) - .conditionalAttr(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.ATK, 4 / 3), - new Ability(AbilityId.HADRON_ENGINE, 9) + .conditionalAttr(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.ATK, 4 / 3) + .build(), + new AbBuilder(AbilityId.HADRON_ENGINE, 9) .attr(PostSummonTerrainChangeAbAttr, TerrainType.ELECTRIC) .attr(PostBiomeChangeTerrainChangeAbAttr, TerrainType.ELECTRIC) - .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), StatMultiplierAbAttr, Stat.SPATK, 4 / 3), - new Ability(AbilityId.OPPORTUNIST, 9) - .attr(StatStageChangeCopyAbAttr), - new Ability(AbilityId.CUD_CHEW, 9) + .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), StatMultiplierAbAttr, Stat.SPATK, 4 / 3) + .build(), + new AbBuilder(AbilityId.OPPORTUNIST, 9) + .attr(StatStageChangeCopyAbAttr) + .build(), + new AbBuilder(AbilityId.CUD_CHEW, 9) .attr(CudChewConsumeBerryAbAttr) - .attr(CudChewRecordBerryAbAttr), - new Ability(AbilityId.SHARPNESS, 9) - .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5), - new Ability(AbilityId.SUPREME_OVERLORD, 9) + .attr(CudChewRecordBerryAbAttr) + .build(), + new AbBuilder(AbilityId.SHARPNESS, 9) + .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5) + .build(), + new AbBuilder(AbilityId.SUPREME_OVERLORD, 9) .conditionalAttr((p) => (p.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints) > 0, PostSummonAddBattlerTagAbAttr, BattlerTagType.SUPREME_OVERLORD, 0, true) - .edgeCase(), // Tag is not tied to ability, so suppression/removal etc will not function until a structure to allow this is implemented - new Ability(AbilityId.COSTAR, 9, -2) - .attr(PostSummonCopyAllyStatsAbAttr), - new Ability(AbilityId.TOXIC_DEBRIS, 9) + .edgeCase() // Tag is not tied to ability, so suppression/removal etc will not function until a structure to allow this is implemented + .build(), + new AbBuilder(AbilityId.COSTAR, 9, -2) + .attr(PostSummonCopyAllyStatsAbAttr) + .build(), + new AbBuilder(AbilityId.TOXIC_DEBRIS, 9) .attr(PostDefendApplyArenaTrapTagAbAttr, (_target, _user, move) => move.category === MoveCategory.PHYSICAL, ArenaTagType.TOXIC_SPIKES) - .bypassFaint(), - new Ability(AbilityId.ARMOR_TAIL, 9) + .bypassFaint() + .build(), + new AbBuilder(AbilityId.ARMOR_TAIL, 9) .attr(FieldPriorityMoveImmunityAbAttr) - .ignorable(), - new Ability(AbilityId.EARTH_EATER, 9) + .ignorable() + .build(), + new AbBuilder(AbilityId.EARTH_EATER, 9) .attr(TypeImmunityHealAbAttr, PokemonType.GROUND) - .ignorable(), - new Ability(AbilityId.MYCELIUM_MIGHT, 9) + .ignorable() + .build(), + new AbBuilder(AbilityId.MYCELIUM_MIGHT, 9) .attr(ChangeMovePriorityAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS, -0.2) .attr(PreventBypassSpeedChanceAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS) - .attr(MoveAbilityBypassAbAttr, (_pokemon, move: Move) => move.category === MoveCategory.STATUS), - new Ability(AbilityId.MINDS_EYE, 9) + .attr(MoveAbilityBypassAbAttr, (_pokemon, move: Move) => move.category === MoveCategory.STATUS) + .build(), + new AbBuilder(AbilityId.MINDS_EYE, 9) .attr(IgnoreTypeImmunityAbAttr, PokemonType.GHOST, [ PokemonType.NORMAL, PokemonType.FIGHTING ]) .attr(ProtectStatAbAttr, Stat.ACC) .attr(IgnoreOpponentStatStagesAbAttr, [ Stat.EVA ]) - .ignorable(), - new Ability(AbilityId.SUPERSWEET_SYRUP, 9) - .attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1), - new Ability(AbilityId.HOSPITALITY, 9, -2) - .attr(PostSummonAllyHealAbAttr, 4, true), - new Ability(AbilityId.TOXIC_CHAIN, 9) - .attr(PostAttackApplyStatusEffectAbAttr, false, 30, StatusEffect.TOXIC), - new Ability(AbilityId.EMBODY_ASPECT_TEAL, 9) + .ignorable() + .build(), + new AbBuilder(AbilityId.SUPERSWEET_SYRUP, 9) + .attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1) + .build(), + new AbBuilder(AbilityId.HOSPITALITY, 9, -2) + .attr(PostSummonAllyHealAbAttr, 4, true) + .build(), + new AbBuilder(AbilityId.TOXIC_CHAIN, 9) + .attr(PostAttackApplyStatusEffectAbAttr, false, 30, StatusEffect.TOXIC) + .build(), + new AbBuilder(AbilityId.EMBODY_ASPECT_TEAL, 9) .attr(PostTeraFormChangeStatChangeAbAttr, [ Stat.SPD ], 1) // Activates immediately upon Terastallizing, as well as upon switching in while Terastallized .conditionalAttr(pokemon => pokemon.isTerastallized, PostSummonStatStageChangeAbAttr, [ Stat.SPD ], 1, true) .uncopiable() .unreplaceable() // TODO is this true? - .attr(NoTransformAbilityAbAttr), - new Ability(AbilityId.EMBODY_ASPECT_WELLSPRING, 9) + .attr(NoTransformAbilityAbAttr) + .build(), + new AbBuilder(AbilityId.EMBODY_ASPECT_WELLSPRING, 9) .attr(PostTeraFormChangeStatChangeAbAttr, [ Stat.SPDEF ], 1) .conditionalAttr(pokemon => pokemon.isTerastallized, PostSummonStatStageChangeAbAttr, [ Stat.SPDEF ], 1, true) .uncopiable() .unreplaceable() - .attr(NoTransformAbilityAbAttr), - new Ability(AbilityId.EMBODY_ASPECT_HEARTHFLAME, 9) + .attr(NoTransformAbilityAbAttr) + .build(), + new AbBuilder(AbilityId.EMBODY_ASPECT_HEARTHFLAME, 9) .attr(PostTeraFormChangeStatChangeAbAttr, [ Stat.ATK ], 1) .conditionalAttr(pokemon => pokemon.isTerastallized, PostSummonStatStageChangeAbAttr, [ Stat.ATK ], 1, true) .uncopiable() .unreplaceable() - .attr(NoTransformAbilityAbAttr), - new Ability(AbilityId.EMBODY_ASPECT_CORNERSTONE, 9) + .attr(NoTransformAbilityAbAttr) + .build(), + new AbBuilder(AbilityId.EMBODY_ASPECT_CORNERSTONE, 9) .attr(PostTeraFormChangeStatChangeAbAttr, [ Stat.DEF ], 1) .conditionalAttr(pokemon => pokemon.isTerastallized, PostSummonStatStageChangeAbAttr, [ Stat.DEF ], 1, true) .uncopiable() .unreplaceable() - .attr(NoTransformAbilityAbAttr), - new Ability(AbilityId.TERA_SHIFT, 9, 2) + .attr(NoTransformAbilityAbAttr) + .build(), + new AbBuilder(AbilityId.TERA_SHIFT, 9, 2) .attr(PostSummonFormChangeAbAttr, p => p.getFormKey() ? 0 : 1) .uncopiable() .unreplaceable() .unsuppressable() .attr(NoTransformAbilityAbAttr) - .attr(NoFusionAbilityAbAttr), - new Ability(AbilityId.TERA_SHELL, 9) + .attr(NoFusionAbilityAbAttr) + .build(), + new AbBuilder(AbilityId.TERA_SHELL, 9) .attr(FullHpResistTypeAbAttr) .uncopiable() .unreplaceable() - .ignorable(), - new Ability(AbilityId.TERAFORM_ZERO, 9) + .ignorable() + .build(), + new AbBuilder(AbilityId.TERAFORM_ZERO, 9) .attr(ClearWeatherAbAttr, [ WeatherType.SUNNY, WeatherType.RAIN, WeatherType.SANDSTORM, WeatherType.HAIL, WeatherType.SNOW, WeatherType.FOG, WeatherType.HEAVY_RAIN, WeatherType.HARSH_SUN, WeatherType.STRONG_WINDS ]) .attr(ClearTerrainAbAttr, [ TerrainType.MISTY, TerrainType.ELECTRIC, TerrainType.GRASSY, TerrainType.PSYCHIC ]) .uncopiable() .unreplaceable() - .condition(getOncePerBattleCondition(AbilityId.TERAFORM_ZERO)), - new Ability(AbilityId.POISON_PUPPETEER, 9) + .condition(getOncePerBattleCondition(AbilityId.TERAFORM_ZERO)) + .build(), + new AbBuilder(AbilityId.POISON_PUPPETEER, 9) .uncopiable() .unreplaceable() // TODO is this true? - .attr(ConfusionOnStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) + .attr(ConfusionOnStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC).build() ); } diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 0fdb0d01e43..c101f60cfdb 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -7513,7 +7513,7 @@ export class AbilityChangeAttr extends MoveEffectAttr { } getCondition(): MoveConditionFunc { - return (user, target, move) => (this.selfTarget ? user : target).getAbility().isReplaceable && (this.selfTarget ? user : target).getAbility().id !== this.ability; + return (user, target, move) => (this.selfTarget ? user : target).getAbility().replaceable && (this.selfTarget ? user : target).getAbility().id !== this.ability; } } @@ -7547,9 +7547,9 @@ export class AbilityCopyAttr extends MoveEffectAttr { getCondition(): MoveConditionFunc { return (user, target, move) => { const ally = user.getAlly(); - let ret = target.getAbility().isCopiable && user.getAbility().isReplaceable; + let ret = target.getAbility().copiable && user.getAbility().replaceable; if (this.copyToPartner && globalScene.currentBattle?.double) { - ret = ret && (!ally?.hp || ally?.getAbility().isReplaceable); + ret = ret && (!ally?.hp || ally?.getAbility().replaceable); } else { ret = ret && user.getAbility().id !== target.getAbility().id; } @@ -7578,7 +7578,7 @@ export class AbilityGiveAttr extends MoveEffectAttr { } getCondition(): MoveConditionFunc { - return (user, target, move) => user.getAbility().isCopiable && target.getAbility().isReplaceable && user.getAbility().id !== target.getAbility().id; + return (user, target, move) => user.getAbility().copiable && target.getAbility().replaceable && user.getAbility().id !== target.getAbility().id; } } @@ -7601,7 +7601,7 @@ export class SwitchAbilitiesAttr extends MoveEffectAttr { } getCondition(): MoveConditionFunc { - return (user, target, move) => [user, target].every(pkmn => pkmn.getAbility().isSwappable); + return (user, target, move) => [user, target].every(pkmn => pkmn.getAbility().swappable); } } @@ -7627,7 +7627,7 @@ export class SuppressAbilitiesAttr extends MoveEffectAttr { /** Causes the effect to fail when the target's ability is unsupressable or already suppressed. */ getCondition(): MoveConditionFunc { - return (_user, target, _move) => !target.summonData.abilitySuppressed && (target.getAbility().isSuppressable || (target.hasPassive() && target.getPassiveAbility().isSuppressable)); + return (_user, target, _move) => !target.summonData.abilitySuppressed && (target.getAbility().suppressable || (target.hasPassive() && target.getPassiveAbility().suppressable)); } } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 3154f273cf5..56f1333e580 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2201,10 +2201,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return false; } const arena = globalScene?.arena; - if (arena.ignoreAbilities && arena.ignoringEffectSource !== this.getBattlerIndex() && ability.isIgnorable) { + if (arena.ignoreAbilities && arena.ignoringEffectSource !== this.getBattlerIndex() && ability.ignorable) { return false; } - if (this.summonData.abilitySuppressed && ability.isSuppressable) { + if (this.summonData.abilitySuppressed && ability.suppressable) { return false; } const suppressAbilitiesTag = arena.getTag(ArenaTagType.NEUTRALIZING_GAS) as SuppressAbilitiesTag; @@ -2216,14 +2216,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { // (Balance decided that the other ability of a neutralizing gas pokemon should not be neutralized) // If the ability itself is neutralizing gas, don't suppress it (handled through arena tag) const unsuppressable = - !ability.isSuppressable + !ability.suppressable || thisAbilitySuppressing || (hasSuppressingAbility && !suppressAbilitiesTag.shouldApplyToSelf()); if (!unsuppressable) { return false; } } - return (this.hp > 0 || ability.isBypassFaint) && !ability.conditions.find(condition => !condition(this)); + return (this.hp > 0 || ability.bypassFaint) && !ability.conditions.find(condition => !condition(this)); } /**