This commit is contained in:
thisPieonFire 2025-08-07 16:30:38 -05:00 committed by GitHub
commit f4f5325a7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 292 additions and 134 deletions

View File

@ -26,3 +26,4 @@ ignore:
- .git
- public
- dist
- .idea

View File

@ -1982,7 +1982,12 @@ export class AllyMoveCategoryPowerBoostAbAttr extends FieldMovePowerBoostAbAttr
* @param powerMultiplier - The multiplier to apply to the move's power.
*/
constructor(boostedCategories: MoveCategory[], powerMultiplier: number) {
super((_pokemon, _defender, move) => boostedCategories.includes(move.category), powerMultiplier);
super((_pokemon, _defender, move) => {
if (_pokemon === null) {
return false;
}
return boostedCategories.includes(_pokemon.getMoveCategory(_defender, move));
}, powerMultiplier);
}
}
@ -2114,7 +2119,13 @@ export abstract class PostAttackAbAttr extends AbAttr {
/** The default `attackCondition` requires that the selected move is a damaging move */
constructor(
attackCondition: PokemonAttackCondition = (_user, _target, move) => move.category !== MoveCategory.STATUS,
attackCondition: PokemonAttackCondition = (_user, _target, move) => {
if (!_user) {
return false;
}
return _user.getMoveCategory(_target, move) !== MoveCategory.STATUS;
},
showAbility = true,
) {
super(showAbility);
@ -6933,7 +6944,12 @@ export function initAbilities() {
.attr(PostSummonAddBattlerTagAbAttr, BattlerTagType.TRUANT, 1, false),
new Ability(AbilityId.HUSTLE, 3)
.attr(StatMultiplierAbAttr, Stat.ATK, 1.5)
.attr(StatMultiplierAbAttr, Stat.ACC, 0.8, (_user, _target, move) => move.category === MoveCategory.PHYSICAL),
.attr(StatMultiplierAbAttr, Stat.ACC, 0.8, (_user, _target, move) => {
if (_user === null) {
return false
}
return _user.getMoveCategory(_target, move) === MoveCategory.PHYSICAL
}),
new Ability(AbilityId.CUTE_CHARM, 3)
.attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED),
new Ability(AbilityId.PLUS, 3)
@ -7165,8 +7181,8 @@ export function initAbilities() {
.attr(AlliedFieldDamageReductionAbAttr, 0.75)
.ignorable(),
new Ability(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),
.attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => _user.getMoveCategory(_target, move) === MoveCategory.PHYSICAL, Stat.DEF, -1)
.attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => _user.getMoveCategory(_target, move) === MoveCategory.PHYSICAL, Stat.SPD, 2),
new Ability(AbilityId.HEAVY_METAL, 5)
.attr(WeightMultiplierAbAttr, 2)
.ignorable(),
@ -7177,9 +7193,15 @@ export function initAbilities() {
.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),
.attr(MovePowerBoostAbAttr, (user, _target, move) => {
if (user===null) { return false; }
return user.getMoveCategory(_target, move) === 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),
.attr(MovePowerBoostAbAttr, (user, _target, move) => {
if (user===null) { return false; }
return user.getMoveCategory(_target, move) === MoveCategory.SPECIAL && user?.status?.effect === StatusEffect.BURN
}, 1.5),
new Ability(AbilityId.HARVEST, 5)
.attr(
PostTurnRestoreBerryAbAttr,
@ -7240,11 +7262,11 @@ export function initAbilities() {
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),
.attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => user.getMoveType(move) === PokemonType.DARK && user.getMoveCategory(_target, move) !== MoveCategory.STATUS, Stat.ATK, 1),
new Ability(AbilityId.RATTLED, 5)
.attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => {
const moveType = user.getMoveType(move);
return move.category !== MoveCategory.STATUS
return user.getMoveCategory(_target, move) !== MoveCategory.STATUS
&& (moveType === PokemonType.DARK || moveType === PokemonType.BUG || moveType === PokemonType.GHOST);
}, Stat.SPD, 1)
.attr(PostIntimidateStatStageChangeAbAttr, [ Stat.SPD ], 1),
@ -7258,7 +7280,7 @@ export function initAbilities() {
.attr(TypeImmunityStatStageChangeAbAttr, PokemonType.GRASS, Stat.ATK, 1)
.ignorable(),
new Ability(AbilityId.PRANKSTER, 5)
.attr(ChangeMovePriorityAbAttr, (_pokemon, move: Move) => move.category === MoveCategory.STATUS, 1),
.attr(ChangeMovePriorityAbAttr, (_pokemon, move: Move) => _pokemon.getMoveCategory(_pokemon, move) === MoveCategory.STATUS, 1),
new Ability(AbilityId.SAND_FORCE, 5)
.attr(MoveTypePowerBoostAbAttr, PokemonType.ROCK, 1.3)
.attr(MoveTypePowerBoostAbAttr, PokemonType.GROUND, 1.3)
@ -7311,8 +7333,15 @@ export function initAbilities() {
// TODO: needs testing on interaction with weather blockage
.edgeCase(),
new Ability(AbilityId.FUR_COAT, 6)
.attr(ReceivedMoveDamageMultiplierAbAttr, (_target, _user, move) => move.category === MoveCategory.PHYSICAL, 0.5)
.attr(ReceivedMoveDamageMultiplierAbAttr, (_target, _user, move) => {
const isPhysicalMove = _user.getMoveCategory(_target, move) === MoveCategory.PHYSICAL
// This is for a theoretical move where Fur Coat shouldn't apply, in case it's implemented later
const moveIsPhysicalAndActuallyHitsDef = isPhysicalMove && !move.hasAttr("VariableDefAttr")
const moveIsSpecialButHitsPhysicalInstead = move.hasAttr("DefDefAttr")
return moveIsPhysicalAndActuallyHitsDef || moveIsSpecialButHitsPhysicalInstead
}, 0.5)
.ignorable(),
new Ability(AbilityId.MAGICIAN, 6)
.attr(PostAttackStealHeldItemAbAttr),
new Ability(AbilityId.BULLETPROOF, 6)
@ -7382,7 +7411,7 @@ export function initAbilities() {
.attr(PreLeaveFieldClearWeatherAbAttr)
.bypassFaint(),
new Ability(AbilityId.STAMINA, 7)
.attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1),
.attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => _user.getMoveCategory(_target, move) !== MoveCategory.STATUS, Stat.DEF, 1),
new Ability(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
@ -7390,7 +7419,7 @@ export function initAbilities() {
.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),
.attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => user.getMoveType(move) === PokemonType.WATER && user.getMoveCategory(_target, move) !== 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)
@ -7419,7 +7448,7 @@ export function initAbilities() {
new Ability(AbilityId.STEELWORKER, 7)
.attr(MoveTypePowerBoostAbAttr, PokemonType.STEEL),
new Ability(AbilityId.BERSERK, 7)
.attr(PostDefendHpGatedStatStageChangeAbAttr, (_target, _user, move) => move.category !== MoveCategory.STATUS, 0.5, [ Stat.SPATK ], 1)
.attr(PostDefendHpGatedStatStageChangeAbAttr, (_target, _user, move) => _user.getMoveCategory(_target, move) !== MoveCategory.STATUS, 0.5, [ Stat.SPATK ], 1)
.condition(getSheerForceHitDisableAbCondition()),
new Ability(AbilityId.SLUSH_RUSH, 7)
.attr(StatMultiplierAbAttr, Stat.SPD, 2)
@ -7573,7 +7602,7 @@ export function initAbilities() {
.attr(FetchBallAbAttr)
.condition(getOncePerBattleCondition(AbilityId.BALL_FETCH)),
new Ability(AbilityId.COTTON_DOWN, 8)
.attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => move.category !== MoveCategory.STATUS, Stat.SPD, -1, false, true)
.attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => _user.getMoveCategory(_target, move) !== MoveCategory.STATUS, Stat.SPD, -1, false, true)
.bypassFaint(),
new Ability(AbilityId.PROPELLER_TAIL, 8)
.attr(BlockRedirectAbAttr),
@ -7598,7 +7627,7 @@ export function initAbilities() {
new Ability(AbilityId.STEAM_ENGINE, 8)
.attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => {
const moveType = user.getMoveType(move);
return move.category !== MoveCategory.STATUS
return user.getMoveCategory(_target, move) !== MoveCategory.STATUS
&& (moveType === PokemonType.FIRE || moveType === PokemonType.WATER);
}, Stat.SPD, 6),
new Ability(AbilityId.PUNK_ROCK, 8)
@ -7606,10 +7635,10 @@ export function initAbilities() {
.attr(ReceivedMoveDamageMultiplierAbAttr, (_target, _user, move) => move.hasFlag(MoveFlags.SOUND_BASED), 0.5)
.ignorable(),
new Ability(AbilityId.SAND_SPIT, 8)
.attr(PostDefendWeatherChangeAbAttr, WeatherType.SANDSTORM, (_target, _user, move) => move.category !== MoveCategory.STATUS)
.attr(PostDefendWeatherChangeAbAttr, WeatherType.SANDSTORM, (_target, _user, move) => _user.getMoveCategory(_target, move) !== MoveCategory.STATUS)
.bypassFaint(),
new Ability(AbilityId.ICE_SCALES, 8)
.attr(ReceivedMoveDamageMultiplierAbAttr, (_target, _user, move) => move.category === MoveCategory.SPECIAL, 0.5)
.attr(ReceivedMoveDamageMultiplierAbAttr, (_target, _user, move) => _user.getMoveCategory(_target, move) === MoveCategory.SPECIAL, 0.5)
.ignorable(),
new Ability(AbilityId.RIPEN, 8)
.attr(DoubleBerryEffectAbAttr),
@ -7623,7 +7652,7 @@ export function initAbilities() {
// When weather changes to HAIL or SNOW while pokemon is fielded, add BattlerTagType.ICE_FACE
.attr(PostWeatherChangeAddBattlerTagAttr, BattlerTagType.ICE_FACE, 0, WeatherType.HAIL, WeatherType.SNOW)
.attr(FormBlockDamageAbAttr,
(target, _user, move) => move.category === MoveCategory.PHYSICAL && !!target.getTag(BattlerTagType.ICE_FACE), 0, BattlerTagType.ICE_FACE,
(target, _user, move) => _user.getMoveCategory(target, move) === MoveCategory.PHYSICAL && !!target.getTag(BattlerTagType.ICE_FACE), 0, BattlerTagType.ICE_FACE,
(pokemon, abilityName) => i18next.t("abilityTriggers:iceFaceAvoidedDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName }))
.attr(PostBattleInitFormChangeAbAttr, () => 0)
.uncopiable()
@ -7703,13 +7732,13 @@ export function initAbilities() {
.attr(PostDefendTerrainChangeAbAttr, TerrainType.GRASSY)
.bypassFaint(),
new Ability(AbilityId.THERMAL_EXCHANGE, 9)
.attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => user.getMoveType(move) === PokemonType.FIRE && move.category !== MoveCategory.STATUS, Stat.ATK, 1)
.attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => user.getMoveType(move) === PokemonType.FIRE && user.getMoveCategory(_target, move) !== MoveCategory.STATUS, Stat.ATK, 1)
.attr(StatusEffectImmunityAbAttr, StatusEffect.BURN)
.attr(PostSummonHealStatusAbAttr, StatusEffect.BURN)
.ignorable(),
new Ability(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)
.attr(PostDefendHpGatedStatStageChangeAbAttr, (_target, _user, move) => _user.getMoveCategory(_target, move) !== MoveCategory.STATUS, 0.5, [ Stat.ATK, Stat.SPATK, Stat.SPD ], 1)
.attr(PostDefendHpGatedStatStageChangeAbAttr, (_target, _user, move) => _user.getMoveCategory(_target, move) !== MoveCategory.STATUS, 0.5, [ Stat.DEF, Stat.SPDEF ], -1)
.condition(getSheerForceHitDisableAbCondition()),
new Ability(AbilityId.PURIFYING_SALT, 9)
.attr(StatusEffectImmunityAbAttr)
@ -7719,7 +7748,7 @@ export function initAbilities() {
.attr(TypeImmunityStatStageChangeAbAttr, PokemonType.FIRE, Stat.DEF, 2)
.ignorable(),
new Ability(AbilityId.WIND_RIDER, 9)
.attr(MoveImmunityStatStageChangeAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.hasFlag(MoveFlags.WIND_MOVE) && move.category !== MoveCategory.STATUS, Stat.ATK, 1)
.attr(MoveImmunityStatStageChangeAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.hasFlag(MoveFlags.WIND_MOVE) && pokemon.getMoveCategory(attacker, move) !== MoveCategory.STATUS, Stat.ATK, 1)
.attr(PostSummonStatStageChangeOnArenaAbAttr, ArenaTagType.TAILWIND)
.ignorable(),
new Ability(AbilityId.GUARD_DOG, 9)
@ -7746,7 +7775,7 @@ export function initAbilities() {
.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),
.attr(PostDefendApplyBattlerTagAbAttr, (_target, _user, move) => _user.getMoveCategory(_target, move) !== MoveCategory.STATUS, BattlerTagType.CHARGED),
new Ability(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)
@ -7760,7 +7789,7 @@ export function initAbilities() {
new Ability(AbilityId.GOOD_AS_GOLD, 9)
.attr(MoveImmunityAbAttr, (pokemon, attacker, move) =>
pokemon !== attacker
&& move.category === MoveCategory.STATUS
&& pokemon.getMoveCategory(attacker, move) === 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
@ -7800,7 +7829,7 @@ export function initAbilities() {
new Ability(AbilityId.COSTAR, 9, -2)
.attr(PostSummonCopyAllyStatsAbAttr),
new Ability(AbilityId.TOXIC_DEBRIS, 9)
.attr(PostDefendApplyArenaTrapTagAbAttr, (_target, _user, move) => move.category === MoveCategory.PHYSICAL, ArenaTagType.TOXIC_SPIKES)
.attr(PostDefendApplyArenaTrapTagAbAttr, (_target, _user, move) => _user.getMoveCategory(_target, move) === MoveCategory.PHYSICAL, ArenaTagType.TOXIC_SPIKES)
.bypassFaint(),
new Ability(AbilityId.ARMOR_TAIL, 9)
.attr(FieldPriorityMoveImmunityAbAttr)
@ -7809,9 +7838,9 @@ export function initAbilities() {
.attr(TypeImmunityHealAbAttr, PokemonType.GROUND)
.ignorable(),
new Ability(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),
.attr(ChangeMovePriorityAbAttr, (_pokemon, move) => _pokemon.getMoveCategory(_pokemon, move) === MoveCategory.STATUS, -0.2)
.attr(PreventBypassSpeedChanceAbAttr, (_pokemon, move) => _pokemon.getMoveCategory(_pokemon, move) === MoveCategory.STATUS)
.attr(MoveAbilityBypassAbAttr, (_pokemon, move: Move) => _pokemon.getMoveCategory(_pokemon, move) === MoveCategory.STATUS),
new Ability(AbilityId.MINDS_EYE, 9)
.attr(IgnoreTypeImmunityAbAttr, PokemonType.GHOST, [ PokemonType.NORMAL, PokemonType.FIGHTING ])
.attr(ProtectStatAbAttr, Stat.ACC)

View File

@ -1,97 +1,101 @@
import { AbAttrParamsWithCancel, PreAttackModifyPowerAbAttrParams } from "#abilities/ability";
import {AbAttrParamsWithCancel, PreAttackModifyPowerAbAttrParams} from "#abilities/ability";
import {applyAbAttrs} from "#abilities/apply-ab-attrs";
import {loggedInUser} from "#app/account";
import type {GameMode} from "#app/game-mode";
import {globalScene} from "#app/global-scene";
import {getPokemonNameWithAffix} from "#app/messages";
import type {ArenaTrapTag} from "#data/arena-tag";
import {WeakenMoveTypeTag} from "#data/arena-tag";
import {MoveChargeAnim} from "#data/battle-anims";
import {
applyAbAttrs
} from "#abilities/apply-ab-attrs";
import { loggedInUser } from "#app/account";
import type { GameMode } from "#app/game-mode";
import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages";
import type { ArenaTrapTag } from "#data/arena-tag";
import { WeakenMoveTypeTag } from "#data/arena-tag";
import { MoveChargeAnim } from "#data/battle-anims";
import {
CommandedTag,
EncoreTag,
GulpMissileTag,
HelpingHandTag,
SemiInvulnerableTag,
ShellTrapTag,
StockpilingTag,
SubstituteTag,
TrappedTag,
TypeBoostTag,
CommandedTag,
EncoreTag,
GulpMissileTag,
HelpingHandTag,
SemiInvulnerableTag,
ShellTrapTag,
StockpilingTag,
SubstituteTag,
TrappedTag,
TypeBoostTag,
} from "#data/battler-tags";
import { getBerryEffectFunc } from "#data/berry";
import { applyChallenges } from "#data/challenge";
import { allAbilities, allMoves } from "#data/data-lists";
import { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-change-triggers";
import { DelayedAttackTag } from "#data/positional-tags/positional-tag";
import {getBerryEffectFunc} from "#data/berry";
import {applyChallenges} from "#data/challenge";
import {allAbilities, allMoves} from "#data/data-lists";
import {SpeciesFormChangeRevertWeatherFormTrigger} from "#data/form-change-triggers";
import {DelayedAttackTag} from "#data/positional-tags/positional-tag";
import {getNonVolatileStatusEffects, getStatusEffectHealText, isNonVolatileStatusEffect,} from "#data/status-effect";
import {TerrainType} from "#data/terrain";
import {getTypeDamageMultiplier} from "#data/type";
import {AbilityId} from "#enums/ability-id";
import {ArenaTagSide} from "#enums/arena-tag-side";
import {ArenaTagType} from "#enums/arena-tag-type";
import {BattleType} from "#enums/battle-type";
import type {BattlerIndex} from "#enums/battler-index";
import {BattlerTagType} from "#enums/battler-tag-type";
import {BiomeId} from "#enums/biome-id";
import {ChallengeType} from "#enums/challenge-type";
import {Command} from "#enums/command";
import {FieldPosition} from "#enums/field-position";
import {HitResult} from "#enums/hit-result";
import {ModifierPoolType} from "#enums/modifier-pool-type";
import {ChargeAnim} from "#enums/move-anims-common";
import {MoveId} from "#enums/move-id";
import {MoveResult} from "#enums/move-result";
import {isVirtual, MoveUseMode} from "#enums/move-use-mode";
import {MoveCategory} from "#enums/move-category";
import {MoveEffectTrigger} from "#enums/move-effect-trigger";
import {MoveFlags} from "#enums/move-flags";
import {MoveTarget} from "#enums/move-target";
import {MultiHitType} from "#enums/multi-hit-type";
import {PokemonType} from "#enums/pokemon-type";
import {PositionalTagType} from "#enums/positional-tag-type";
import {SpeciesId} from "#enums/species-id";
import {BATTLE_STATS, type BattleStat, type EffectiveStat, getStatKey, Stat,} from "#enums/stat";
import {StatusEffect} from "#enums/status-effect";
import {SwitchType} from "#enums/switch-type";
import {WeatherType} from "#enums/weather-type";
import {MoveUsedEvent} from "#events/battle-scene";
import type {EnemyPokemon, Pokemon} from "#field/pokemon";
import {
getNonVolatileStatusEffects,
getStatusEffectHealText,
isNonVolatileStatusEffect,
} from "#data/status-effect";
import { TerrainType } from "#data/terrain";
import { getTypeDamageMultiplier } from "#data/type";
import { AbilityId } from "#enums/ability-id";
import { ArenaTagSide } from "#enums/arena-tag-side";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattleType } from "#enums/battle-type";
import type { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type";
import { BiomeId } from "#enums/biome-id";
import { ChallengeType } from "#enums/challenge-type";
import { Command } from "#enums/command";
import { FieldPosition } from "#enums/field-position";
import { HitResult } from "#enums/hit-result";
import { ModifierPoolType } from "#enums/modifier-pool-type";
import { ChargeAnim } from "#enums/move-anims-common";
import { MoveId } from "#enums/move-id";
import { MoveResult } from "#enums/move-result";
import { isVirtual, MoveUseMode } from "#enums/move-use-mode";
import { MoveCategory } from "#enums/move-category";
import { MoveEffectTrigger } from "#enums/move-effect-trigger";
import { MoveFlags } from "#enums/move-flags";
import { MoveTarget } from "#enums/move-target";
import { MultiHitType } from "#enums/multi-hit-type";
import { PokemonType } from "#enums/pokemon-type";
import { PositionalTagType } from "#enums/positional-tag-type";
import { SpeciesId } from "#enums/species-id";
import {
BATTLE_STATS,
type BattleStat,
type EffectiveStat,
getStatKey,
Stat,
} from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import { SwitchType } from "#enums/switch-type";
import { WeatherType } from "#enums/weather-type";
import { MoveUsedEvent } from "#events/battle-scene";
import type { EnemyPokemon, Pokemon } from "#field/pokemon";
import {
AttackTypeBoosterModifier,
BerryModifier,
PokemonHeldItemModifier,
PokemonMoveAccuracyBoosterModifier,
PokemonMultiHitModifier,
PreserveBerryModifier,
AttackTypeBoosterModifier,
BerryModifier,
PokemonHeldItemModifier,
PokemonMoveAccuracyBoosterModifier,
PokemonMultiHitModifier,
PreserveBerryModifier,
} from "#modifiers/modifier";
import { applyMoveAttrs } from "#moves/apply-attrs";
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves";
import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils";
import { PokemonMove } from "#moves/pokemon-move";
import { MoveEndPhase } from "#phases/move-end-phase";
import { MovePhase } from "#phases/move-phase";
import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
import { SwitchSummonPhase } from "#phases/switch-summon-phase";
import type { AttackMoveResult } from "#types/attack-move-result";
import type { Localizable } from "#types/locales";
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString } from "#types/move-types";
import type { TurnMove } from "#types/turn-move";
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
import { getEnumValues } from "#utils/enums";
import { toTitleCase } from "#utils/strings";
import {applyMoveAttrs} from "#moves/apply-attrs";
import {
invalidAssistMoves,
invalidCopycatMoves,
invalidMetronomeMoves,
invalidMirrorMoveMoves,
invalidSketchMoves,
invalidSleepTalkMoves
} from "#moves/invalid-moves";
import {frenzyMissFunc, getMoveTargets} from "#moves/move-utils";
import {PokemonMove} from "#moves/pokemon-move";
import {MoveEndPhase} from "#phases/move-end-phase";
import {MovePhase} from "#phases/move-phase";
import {PokemonHealPhase} from "#phases/pokemon-heal-phase";
import {SwitchSummonPhase} from "#phases/switch-summon-phase";
import type {AttackMoveResult} from "#types/attack-move-result";
import type {Localizable} from "#types/locales";
import type {ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString} from "#types/move-types";
import type {TurnMove} from "#types/turn-move";
import {
BooleanHolder,
type Constructor,
isNullOrUndefined,
NumberHolder,
randSeedFloat,
randSeedInt,
randSeedItem,
toDmgValue
} from "#utils/common";
import {getEnumValues} from "#utils/enums";
import {toTitleCase} from "#utils/strings";
import i18next from "i18next";
/**
@ -1626,7 +1630,7 @@ export class MatchHpAttr extends FixedDamageAttr {
}*/
}
type MoveFilter = (move: Move) => boolean;
type MoveFilter = (user: Pokemon, target: Pokemon, move: Move) => boolean;
export class CounterDamageAttr extends FixedDamageAttr {
private moveFilter: MoveFilter;
@ -1640,14 +1644,14 @@ export class CounterDamageAttr extends FixedDamageAttr {
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const damage = user.turnData.attacksReceived.filter(ar => this.moveFilter(allMoves[ar.move])).reduce((total: number, ar: AttackMoveResult) => total + ar.damage, 0);
const damage = user.turnData.attacksReceived.filter(ar => this.moveFilter(user, target, allMoves[ar.move])).reduce((total: number, ar: AttackMoveResult) => total + ar.damage, 0);
(args[0] as NumberHolder).value = toDmgValue(damage * this.multiplier);
return true;
}
getCondition(): MoveConditionFunc {
return (user, target, move) => !!user.turnData.attacksReceived.filter(ar => this.moveFilter(allMoves[ar.move])).length;
return (user, target, move) => !!user.turnData.attacksReceived.filter(ar => this.moveFilter(user, target, allMoves[ar.move])).length;
}
}
@ -2534,10 +2538,10 @@ export class StatusEffectAttr extends MoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
const statusCheck = moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance;
const quiet = move.category !== MoveCategory.STATUS;
const quiet = user.getMoveCategory(target, move) !== MoveCategory.STATUS;
if (statusCheck) {
const pokemon = this.selfTarget ? user : target;
if (user !== target && move.category === MoveCategory.STATUS && !target.canSetStatus(this.effect, quiet, false, user, true)) {
if (user !== target && user.getMoveCategory(target, move) === MoveCategory.STATUS && !target.canSetStatus(this.effect, quiet, false, user, true)) {
return false;
}
if (((!pokemon.status || this.overrideStatus) || (pokemon.status.effect === this.effect && moveChance < 0))
@ -5882,7 +5886,7 @@ export class ConfuseAttr extends AddBattlerTagAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!this.selfTarget && target.isSafeguarded(user)) {
if (move.category === MoveCategory.STATUS) {
if (user.getMoveCategory(target, move) === MoveCategory.STATUS) {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) }));
}
return false;
@ -5916,8 +5920,8 @@ export class ProtectAttr extends AddBattlerTagAttr {
for (const turnMove of user.getLastXMoves(-1).slice()) {
if (
// Quick & Wide guard increment the Protect counter without using it for fail chance
!(allMoves[turnMove.move].hasAttr("ProtectAttr") ||
[MoveId.QUICK_GUARD, MoveId.WIDE_GUARD].includes(turnMove.move)) ||
!(allMoves[turnMove.move].hasAttr("ProtectAttr") ||
[MoveId.QUICK_GUARD, MoveId.WIDE_GUARD].includes(turnMove.move)) ||
turnMove.result !== MoveResult.SUCCESS
) {
break;
@ -6498,7 +6502,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
}
getCondition(): MoveConditionFunc {
return (user, target, move) => (move.category !== MoveCategory.STATUS || this.getSwitchOutCondition()(user, target, move));
return (user, target, move) => (user.getMoveCategory(target, move) !== MoveCategory.STATUS || this.getSwitchOutCondition()(user, target, move));
}
getFailedText(_user: Pokemon, target: Pokemon, _move: Move): string | undefined {
@ -6555,7 +6559,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
return !this.isBatonPass()
&& globalScene.currentBattle.waveIndex % 10 !== 0
// Don't allow wild mons to flee with U-turn et al.
&& !(this.selfSwitch && MoveCategory.STATUS !== move.category);
&& !(this.selfSwitch && MoveCategory.STATUS !== user.getMoveCategory(target, move));
}
const party = player ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
@ -8685,7 +8689,7 @@ export function initMoves() {
new AttackMove(MoveId.LOW_KICK, PokemonType.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 20, -1, 0, 1)
.attr(WeightPowerAttr),
new AttackMove(MoveId.COUNTER, PokemonType.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 20, -1, -5, 1)
.attr(CounterDamageAttr, (move: Move) => move.category === MoveCategory.PHYSICAL, 2)
.attr(CounterDamageAttr, (user: Pokemon, target: Pokemon, move: Move) => user.getMoveCategory(target, move) === MoveCategory.PHYSICAL, 2)
.target(MoveTarget.ATTACKER),
new AttackMove(MoveId.SEISMIC_TOSS, PokemonType.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 20, -1, 0, 1)
.attr(LevelDamageAttr),
@ -9250,7 +9254,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.bitingMove(),
new AttackMove(MoveId.MIRROR_COAT, PokemonType.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 20, -1, -5, 2)
.attr(CounterDamageAttr, (move: Move) => move.category === MoveCategory.SPECIAL, 2)
.attr(CounterDamageAttr, (user: Pokemon, target: Pokemon, move: Move) => user.getMoveCategory(target, move) === MoveCategory.SPECIAL, 2)
.target(MoveTarget.ATTACKER),
new StatusMove(MoveId.PSYCH_UP, PokemonType.NORMAL, -1, 10, -1, 0, 2)
.ignoresSubstitute()
@ -9661,7 +9665,7 @@ export function initMoves() {
.attr(AcupressureStatStageChangeAttr)
.target(MoveTarget.USER_OR_NEAR_ALLY),
new AttackMove(MoveId.METAL_BURST, PokemonType.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 4)
.attr(CounterDamageAttr, (move: Move) => (move.category === MoveCategory.PHYSICAL || move.category === MoveCategory.SPECIAL), 1.5)
.attr(CounterDamageAttr, (user: Pokemon, target: Pokemon, move: Move) => (user.getMoveCategory(target, move) === MoveCategory.PHYSICAL || user.getMoveCategory(target, move) === MoveCategory.SPECIAL), 1.5)
.redirectCounter()
.makesContact(false)
.target(MoveTarget.ATTACKER),
@ -11441,7 +11445,7 @@ export function initMoves() {
return !turnMove.length || turnMove[0].move !== move.id || turnMove[0].result !== MoveResult.SUCCESS;
}), // TODO Add Instruct/Encore interaction
new AttackMove(MoveId.COMEUPPANCE, PokemonType.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 9)
.attr(CounterDamageAttr, (move: Move) => (move.category === MoveCategory.PHYSICAL || move.category === MoveCategory.SPECIAL), 1.5)
.attr(CounterDamageAttr, (user: Pokemon, target: Pokemon, move: Move) => (user.getMoveCategory(target, move) === MoveCategory.PHYSICAL || user.getMoveCategory(target, move) === MoveCategory.SPECIAL), 1.5)
.redirectCounter()
.target(MoveTarget.ATTACKER),
new AttackMove(MoveId.AQUA_CUTTER, PokemonType.WATER, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 9)

View File

@ -1403,7 +1403,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* @param move - The {@linkcode Move} being used
* @returns The given move's final category
*/
getMoveCategory(target: Pokemon, move: Move): MoveCategory {
getMoveCategory(target: Pokemon | null, move: Move): MoveCategory {
const moveCategory = new NumberHolder(move.category);
applyMoveAttrs("VariableMoveCategoryAttr", this, target, move, moveCategory);
return moveCategory.value;

View File

@ -0,0 +1,124 @@
import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { GameManager } from "#test/test-utils/game-manager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Ability - Fur Coat", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleStyle("single")
.ability(AbilityId.BALL_FETCH)
.startingLevel(50)
.enemySpecies(SpeciesId.CHANSEY)
.enemyAbility(AbilityId.BALL_FETCH)
.enemyPassiveAbility(AbilityId.NONE)
.enemyMoveset([MoveId.SPLASH])
.enemyLevel(50)
.criticalHits(false);
});
function damageAfterShouldBeAboutHalfOfDamageBefore(damageAfter: number, damageBefore: number) {
const halfDamage = damageBefore * 0.5;
const difference = Math.abs(damageAfter - halfDamage);
// Can't use `toBeCloseTo` because they're exactly 0.5 apart, and we still get rounding errors
expect(difference).toBeLessThan(0.6);
}
it("should reduce damage from a physical move after gaining Fur Coat", async () => {
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
const enemy = game.field.getEnemyPokemon();
// Use Tackle before Fur Coat
game.move.use(MoveId.TACKLE);
await game.toEndOfTurn();
const damageBefore = enemy.getMaxHp() - enemy.hp;
// Give Fur Coat
game.field.mockAbility(enemy, AbilityId.FUR_COAT);
enemy.hp = enemy.getMaxHp();
// Use Tackle after Fur Coat
game.move.use(MoveId.TACKLE);
await game.toEndOfTurn();
const damageAfter = enemy.getMaxHp() - enemy.hp;
damageAfterShouldBeAboutHalfOfDamageBefore(damageAfter, damageBefore);
});
it("should not reduce damage from a special move", async () => {
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
const enemy = game.field.getEnemyPokemon();
// Use Scald before Fur Coat
game.move.use(MoveId.SCALD);
await game.toEndOfTurn();
const damageBefore = enemy.getMaxHp() - enemy.hp;
// Give Fur Coat
game.field.mockAbility(enemy, AbilityId.FUR_COAT);
enemy.hp = enemy.getMaxHp();
// Use Scald after Fur Coat
game.move.use(MoveId.SCALD);
await game.toEndOfTurn();
const damageAfter = enemy.getMaxHp() - enemy.hp;
expect(damageAfter).toBe(damageBefore);
});
it.each([
{ moveName: "Psyshock", moveId: MoveId.PSYSHOCK },
{
moveName: "Psystrike",
moveId: MoveId.PSYSTRIKE,
},
{
moveName: "Secret Sword",
moveId: MoveId.SECRET_SWORD,
},
])("should reduce damage from $moveName after gaining Fur Coat", async ({ moveId }) => {
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
const enemy = game.field.getEnemyPokemon();
game.move.use(moveId);
await game.toEndOfTurn();
const attackDamageBefore = enemy.getMaxHp() - enemy.hp;
game.field.mockAbility(enemy, AbilityId.FUR_COAT);
enemy.hp = enemy.getMaxHp();
game.move.use(moveId);
await game.toEndOfTurn();
const attackDamageAfter = enemy.getMaxHp() - enemy.hp;
damageAfterShouldBeAboutHalfOfDamageBefore(attackDamageAfter, attackDamageBefore);
});
it("should reduce damage from Shell Side Arm", async () => {
await game.classicMode.startBattle([SpeciesId.HERACROSS]);
game.override.enemyAbility(AbilityId.IMMUNITY);
const enemy = game.field.getEnemyPokemon();
// Use Shell Side Arm before Fur Coat
game.move.use(MoveId.SHELL_SIDE_ARM);
await game.toEndOfTurn();
const damageBefore = enemy.getMaxHp() - enemy.hp;
// Give Fur Coat
game.field.mockAbility(enemy, AbilityId.FUR_COAT);
enemy.hp = enemy.getMaxHp();
// Use Shell Side Arm after Fur Coat
game.move.use(MoveId.SHELL_SIDE_ARM);
await game.toEndOfTurn();
const damageAfter = enemy.getMaxHp() - enemy.hp;
damageAfterShouldBeAboutHalfOfDamageBefore(damageAfter, damageBefore);
});
});