From c52e0ac5b6e87d48e53fd8a2505610642b382ad2 Mon Sep 17 00:00:00 2001 From: NxKarim <43686802+NxKarim@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:30:42 -0600 Subject: [PATCH] [Ability] Serene Grace, Shield Dust, Sheer Force (P) (#246) Co-authored-by: okimin --- src/data/ability.ts | 112 +++++++++++-- src/data/move.ts | 162 ++++++++++-------- src/modifier/modifier.ts | 17 +- src/phases.ts | 106 ++++++------ src/test/abilities/serene_grace.test.ts | 110 +++++++++++++ src/test/abilities/sheer_force.test.ts | 208 ++++++++++++++++++++++++ src/test/abilities/shield_dust.test.ts | 79 +++++++++ 7 files changed, 657 insertions(+), 137 deletions(-) mode change 100755 => 100644 src/data/ability.ts create mode 100644 src/test/abilities/serene_grace.test.ts create mode 100644 src/test/abilities/sheer_force.test.ts create mode 100644 src/test/abilities/shield_dust.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts old mode 100755 new mode 100644 index a735c0f54b5..e1d047c7806 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1045,6 +1045,56 @@ export class PreAttackAbAttr extends AbAttr { } } +/** + * Modifies moves additional effects with multipliers, ie. Sheer Force, Serene Grace. + * @extends AbAttr + * @see {@linkcode apply} + */ +export class MoveEffectChanceMultiplierAbAttr extends AbAttr { + private chanceMultiplier: number; + + constructor(chanceMultiplier?: number) { + super(true); + this.chanceMultiplier = chanceMultiplier; + } + /** + * @param args [0]: {@linkcode Utils.NumberHolder} Move additional effect chance. Has to be higher than or equal to 0. + * [1]: {@linkcode Moves } Move used by the ability user. + */ + apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { + + if ((args[0] as Utils.NumberHolder).value <= 0 || (args[1] as Move).id === Moves.ORDER_UP) { + return false; + } + + (args[0] as Utils.NumberHolder).value *= this.chanceMultiplier; + (args[0] as Utils.NumberHolder).value = Math.min((args[0] as Utils.NumberHolder).value, 100); + return true; + + } +} + +/** + * Sets incoming moves additional effect chance to zero, ignoring all effects from moves. ie. Shield Dust. + * @extends PreDefendAbAttr + * @see {@linkcode applyPreDefend} + */ +export class IgnoreMoveEffectsAbAttr extends PreDefendAbAttr { + /** + * @param args [0]: {@linkcode Utils.NumberHolder} Move additional effect chance. + */ + applyPreDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { + + if ((args[0] as Utils.NumberHolder).value <= 0) { + return false; + } + + (args[0] as Utils.NumberHolder).value = 0; + return true; + + } +} + export class VariableMovePowerAbAttr extends PreAttackAbAttr { applyPreAttack(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: Move, args: any[]): boolean { //const power = args[0] as Utils.NumberHolder; @@ -1418,7 +1468,8 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr { } applyPostAttack(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (pokemon !== attacker && (!this.contactRequired || move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) && pokemon.randSeedInt(100) < this.chance && !pokemon.status) { + /**Status inflicted by abilities post attacking are also considered additional effects.*/ + if (!attacker.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && pokemon !== attacker && (!this.contactRequired || move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) && pokemon.randSeedInt(100) < this.chance && !pokemon.status) { const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randSeedInt(this.effects.length)]; return attacker.trySetStatus(effect, true, pokemon); } @@ -1448,10 +1499,9 @@ export class PostAttackApplyBattlerTagAbAttr extends PostAttackAbAttr { } applyPostAttack(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (pokemon !== attacker && (!this.contactRequired || move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) && pokemon.randSeedInt(100) < this.chance(attacker, pokemon, move) && !pokemon.status) { + /**Battler tags inflicted by abilities post attacking are also considered additional effects.*/ + if (!attacker.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && pokemon !== attacker && (!this.contactRequired || move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) && pokemon.randSeedInt(100) < this.chance(attacker, pokemon, move) && !pokemon.status) { const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randSeedInt(this.effects.length)]; - - return attacker.addTag(effect); } @@ -2402,6 +2452,35 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr { } } +/** + * Condition function to applied to abilities related to Sheer Force. + * Checks if last move used against target was affected by a Sheer Force user and: + * Disables: Color Change, Pickpocket, Wimp Out, Emergency Exit, Berserk, Anger Shell + * @returns {AbAttrCondition} If false disables the ability which the condition is applied to. + */ +function getSheerForceHitDisableAbCondition(): AbAttrCondition { + return (pokemon: Pokemon) => { + if (!pokemon.turnData) { + return true; + } + + const lastReceivedAttack = pokemon.turnData.attacksReceived[0]; + if (!lastReceivedAttack) { + return true; + } + + const lastAttacker = pokemon.getOpponents().find(p => p.id === lastReceivedAttack.sourceId); + if (!lastAttacker) { + return true; + } + + /**if the last move chance is greater than or equal to cero, and the last attacker's ability is sheer force*/ + const SheerForceAffected = allMoves[lastReceivedAttack.move].chance >= 0 && lastAttacker.hasAbility(Abilities.SHEER_FORCE); + + return !SheerForceAffected; + }; +} + function getWeatherCondition(...weatherTypes: WeatherType[]): AbAttrCondition { return (pokemon: Pokemon) => { if (pokemon.scene.arena.weather?.isEffectSuppressed(pokemon.scene)) { @@ -3932,7 +4011,8 @@ export function initAbilities() { .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY) .ignorable(), new Ability(Abilities.COLOR_CHANGE, 3) - .attr(PostDefendTypeChangeAbAttr), + .attr(PostDefendTypeChangeAbAttr) + .condition(getSheerForceHitDisableAbCondition()), new Ability(Abilities.IMMUNITY, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) .ignorable(), @@ -3940,8 +4020,8 @@ export function initAbilities() { .attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1, (pokemon: Pokemon) => !pokemon.status || pokemon.status.effect !== StatusEffect.FREEZE) .ignorable(), new Ability(Abilities.SHIELD_DUST, 3) - .ignorable() - .unimplemented(), + .attr(IgnoreMoveEffectsAbAttr) + .partial(), new Ability(Abilities.OWN_TEMPO, 3) .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED) .attr(IntimidateImmunityAbAttr) @@ -3984,7 +4064,8 @@ export function initAbilities() { .attr(TypeImmunityStatChangeAbAttr, Type.ELECTRIC, BattleStat.SPATK, 1) .ignorable(), new Ability(Abilities.SERENE_GRACE, 3) - .unimplemented(), + .attr(MoveEffectChanceMultiplierAbAttr, 2) + .partial(), new Ability(Abilities.SWIFT_SWIM, 3) .attr(BattleStatMultiplierAbAttr, BattleStat.SPD, 2) .condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)), @@ -4260,9 +4341,12 @@ export function initAbilities() { new Ability(Abilities.BAD_DREAMS, 4) .attr(PostTurnHurtIfSleepingAbAttr), new Ability(Abilities.PICKPOCKET, 5) - .attr(PostDefendStealHeldItemAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT)), + .attr(PostDefendStealHeldItemAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT)) + .condition(getSheerForceHitDisableAbCondition()), new Ability(Abilities.SHEER_FORCE, 5) - .unimplemented(), + .attr(MovePowerBoostAbAttr, (user, target, move) => move.chance >= 1, 5461/4096) + .attr(MoveEffectChanceMultiplierAbAttr, 0) + .partial(), new Ability(Abilities.CONTRARY, 5) .attr(StatChangeMultiplierAbAttr, -1) .ignorable(), @@ -4471,8 +4555,10 @@ export function initAbilities() { new Ability(Abilities.STAMINA, 7) .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, BattleStat.DEF, 1), new Ability(Abilities.WIMP_OUT, 7) + .condition(getSheerForceHitDisableAbCondition()) .unimplemented(), new Ability(Abilities.EMERGENCY_EXIT, 7) + .condition(getSheerForceHitDisableAbCondition()) .unimplemented(), new Ability(Abilities.WATER_COMPACTION, 7) .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.type === Type.WATER && move.category !== MoveCategory.STATUS, BattleStat.DEF, 2), @@ -4498,7 +4584,8 @@ export function initAbilities() { new Ability(Abilities.STEELWORKER, 7) .attr(MoveTypePowerBoostAbAttr, Type.STEEL), new Ability(Abilities.BERSERK, 7) - .attr(PostDefendHpGatedStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [BattleStat.SPATK], 1), + .attr(PostDefendHpGatedStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [BattleStat.SPATK], 1) + .condition(getSheerForceHitDisableAbCondition()), new Ability(Abilities.SLUSH_RUSH, 7) .attr(BattleStatMultiplierAbAttr, BattleStat.SPD, 2) .condition(getWeatherCondition(WeatherType.HAIL, WeatherType.SNOW)), @@ -4757,7 +4844,8 @@ export function initAbilities() { .ignorable(), new Ability(Abilities.ANGER_SHELL, 9) .attr(PostDefendHpGatedStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [ BattleStat.ATK, BattleStat.SPATK, BattleStat.SPD ], 1) - .attr(PostDefendHpGatedStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [ BattleStat.DEF, BattleStat.SPDEF ], -1), + .attr(PostDefendHpGatedStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [ BattleStat.DEF, BattleStat.SPDEF ], -1) + .condition(getSheerForceHitDisableAbCondition()), new Ability(Abilities.PURIFYING_SALT, 9) .attr(StatusEffectImmunityAbAttr) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.GHOST, 0.5) diff --git a/src/data/move.ts b/src/data/move.ts index 34d64753978..3762a377fc6 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -9,7 +9,7 @@ import { Type } from "./type"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; -import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr, applyPostDefendAbAttrs, PostDefendContactApplyStatusEffectAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr } from "./ability"; +import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr, applyPostDefendAbAttrs, PostDefendContactApplyStatusEffectAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, IgnoreMoveEffectsAbAttr, applyPreDefendAbAttrs, MoveEffectChanceMultiplierAbAttr } from "./ability"; import { allAbilities } from "./ability"; import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier } from "../modifier/modifier"; import { BattlerIndex } from "../battle"; @@ -829,6 +829,22 @@ export class MoveEffectAttr extends MoveAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise { return this.canApply(user, target, move, args); } + + /** + * Gets the used move's additional effect chance. + * If user's ability has MoveEffectChanceMultiplierAbAttr or IgnoreMoveEffectsAbAttr modifies the base chance. + * @param user {@linkcode Pokemon} using this move + * @param target {@linkcode Pokemon} target of this move + * @param move {@linkcode Move} being used + * @param selfEffect {@linkcode Boolean} if move targets user. + * @returns Move chance value. + */ + getMoveChance(user: Pokemon, target: Pokemon, move: Move, selfEffect?: Boolean): integer { + const moveChance = new Utils.NumberHolder(move.chance); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, moveChance, move, target, selfEffect); + applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr,target,user,null,null, moveChance); + return moveChance.value; + } } export class PreMoveMessageAttr extends MoveAttr { @@ -1673,7 +1689,8 @@ export class StatusEffectAttr extends MoveEffectAttr { } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const statusCheck = move.chance < 0 || move.chance === 100 || user.randSeedInt(100) < move.chance; + const moveChance = this.getMoveChance(user,target,move,this.selfTarget); + const statusCheck = moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance; if (statusCheck) { const pokemon = this.selfTarget ? user : target; if (pokemon.status) { @@ -1683,7 +1700,7 @@ export class StatusEffectAttr extends MoveEffectAttr { return false; } } - if ((!pokemon.status || (pokemon.status.effect === this.effect && move.chance < 0)) + if ((!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0)) && pokemon.trySetStatus(this.effect, true, user, this.cureTurn)) { applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null,this.effect); return true; @@ -1693,7 +1710,8 @@ export class StatusEffectAttr extends MoveEffectAttr { } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(this.effect, true, false, user) ? Math.floor(move.chance * -0.1) : 0; + const moveChance = this.getMoveChance(user,target,move,this.selfTarget); + return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(this.effect, true, false, user) ? Math.floor(moveChance * -0.1) : 0; } } @@ -1712,7 +1730,8 @@ export class MultiStatusEffectAttr extends StatusEffectAttr { } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(this.effect, true, false, user) ? Math.floor(move.chance * -0.1) : 0; + const moveChance = this.getMoveChance(user,target,move,this.selfTarget); + return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(this.effect, true, false, user) ? Math.floor(moveChance * -0.1) : 0; } } @@ -2315,7 +2334,8 @@ export class StatChangeAttr extends MoveEffectAttr { return false; } - if (move.chance < 0 || move.chance === 100 || user.randSeedInt(100) < move.chance) { + const moveChance = this.getMoveChance(user,target,move,this.selfTarget); + if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) { const levels = this.getLevels(user); user.scene.unshiftPhase(new StatChangePhase(user.scene, (this.selfTarget ? user : target).getBattlerIndex(), this.selfTarget, this.stats, levels, this.showMessage)); return true; @@ -3892,18 +3912,14 @@ export class AddBattlerTagAttr extends MoveEffectAttr { return false; } - const chance = this.getTagChance(user, target, move); - if (chance < 0 || chance === 100 || user.randSeedInt(100) < chance) { + const moveChance = this.getMoveChance(user,target,move,this.selfTarget); + if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) { return (this.selfTarget ? user : target).addTag(this.tagType, user.randSeedInt(this.turnCountMax - this.turnCountMin, this.turnCountMin), move.id, user.id); } return false; } - getTagChance(user: Pokemon, target: Pokemon, move: Move): integer { - return move.chance; - } - getCondition(): MoveConditionFunc { return this.failOnOverlap ? (user, target, move) => !(this.selfTarget ? user : target).getTag(this.tagType) @@ -3953,11 +3969,11 @@ export class AddBattlerTagAttr extends MoveEffectAttr { } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { - let chance = this.getTagChance(user, target, move); - if (chance < 0) { - chance = 100; + let moveChance = this.getMoveChance(user,target,move,this.selfTarget); + if (moveChance < 0) { + moveChance = 100; } - return Math.floor(this.getTagTargetBenefitScore(user, target, move) * (chance / 100)); + return Math.floor(this.getTagTargetBenefitScore(user, target, move) * (moveChance / 100)); } } @@ -4221,10 +4237,14 @@ export class RemoveArenaTagsAttr extends MoveEffectAttr { export class AddArenaTrapTagAttr extends AddArenaTagAttr { getCondition(): MoveConditionFunc { return (user, target, move) => { + const moveChance = this.getMoveChance(user,target,move,this.selfTarget); const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; - if (move.category !== MoveCategory.STATUS || !user.scene.arena.getTagOnSide(this.tagType, side)) { - return true; + if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) { + if (move.category !== MoveCategory.STATUS || !user.scene.arena.getTagOnSide(this.tagType, side)) { + return true; + } } + const tag = user.scene.arena.getTagOnSide(this.tagType, side) as ArenaTrapTag; return tag.layers < tag.maxLayers; }; @@ -5617,7 +5637,7 @@ export function initMoves() { .attr(ChargeAttr, ChargeAnim.FLY_CHARGING, "flew\nup high!", BattlerTagType.FLYING) .condition(failOnGravityCondition) .ignoresVirtual(), - new AttackMove(Moves.BIND, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, 100, 0, 1) + new AttackMove(Moves.BIND, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1) .attr(TrapAttr, BattlerTagType.BIND), new AttackMove(Moves.SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 80, 75, 20, -1, 0, 1), new AttackMove(Moves.VINE_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 45, 100, 25, -1, 0, 1), @@ -5650,7 +5670,7 @@ export function initMoves() { .attr(MinimizeAccuracyAttr) .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) .attr(StatusEffectAttr, StatusEffect.PARALYSIS), - new AttackMove(Moves.WRAP, Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, 100, 0, 1) + new AttackMove(Moves.WRAP, Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, -1, 0, 1) .attr(TrapAttr, BattlerTagType.WRAP), new AttackMove(Moves.TAKE_DOWN, Type.NORMAL, MoveCategory.PHYSICAL, 90, 85, 20, -1, 0, 1) .attr(RecoilAttr) @@ -5675,7 +5695,7 @@ export function initMoves() { new AttackMove(Moves.PIN_MISSILE, Type.BUG, MoveCategory.PHYSICAL, 25, 95, 20, -1, 0, 1) .attr(MultiHitAttr) .makesContact(false), - new StatusMove(Moves.LEER, Type.NORMAL, 100, 30, 100, 0, 1) + new StatusMove(Moves.LEER, Type.NORMAL, 100, 30, -1, 0, 1) .attr(StatChangeAttr, BattleStat.DEF, -1) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.BITE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, 30, 0, 1) @@ -5784,7 +5804,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.DRAGON_RAGE, Type.DRAGON, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 1) .attr(FixedDamageAttr, 40), - new AttackMove(Moves.FIRE_SPIN, Type.FIRE, MoveCategory.SPECIAL, 35, 85, 15, 100, 0, 1) + new AttackMove(Moves.FIRE_SPIN, Type.FIRE, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 1) .attr(TrapAttr, BattlerTagType.FIRE_SPIN), new AttackMove(Moves.THUNDER_SHOCK, Type.ELECTRIC, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) .attr(StatusEffectAttr, StatusEffect.PARALYSIS), @@ -5900,11 +5920,11 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.BURN), new AttackMove(Moves.WATERFALL, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 15, 20, 0, 1) .attr(FlinchAttr), - new AttackMove(Moves.CLAMP, Type.WATER, MoveCategory.PHYSICAL, 35, 85, 15, 100, 0, 1) + new AttackMove(Moves.CLAMP, Type.WATER, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 1) .attr(TrapAttr, BattlerTagType.CLAMP), new AttackMove(Moves.SWIFT, Type.NORMAL, MoveCategory.SPECIAL, 60, -1, 20, -1, 0, 1) .target(MoveTarget.ALL_NEAR_ENEMIES), - new AttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, 100, 0, 1) + new AttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, -1, 0, 1) .attr(ChargeAttr, ChargeAnim.SKULL_BASH_CHARGING, "lowered\nits head!", null, true) .attr(StatChangeAttr, BattleStat.DEF, 1, true) .ignoresVirtual(), @@ -6253,7 +6273,7 @@ export function initMoves() { .attr(DelayedAttackAttr, ArenaTagType.FUTURE_SIGHT, ChargeAnim.FUTURE_SIGHT_CHARGING, "foresaw\nan attack!"), new AttackMove(Moves.ROCK_SMASH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 15, 50, 0, 2) .attr(StatChangeAttr, BattleStat.DEF, -1), - new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, 100, 0, 2) + new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2) .attr(TrapAttr, BattlerTagType.WHIRLPOOL) .attr(HitsTagAttr, BattlerTagType.UNDERWATER, true), new AttackMove(Moves.BEAT_UP, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 2) @@ -6329,7 +6349,7 @@ export function initMoves() { .ignoresVirtual(), new SelfStatusMove(Moves.INGRAIN, Type.GRASS, -1, 20, -1, 0, 3) .attr(AddBattlerTagAttr, BattlerTagType.INGRAIN, true, true), - new AttackMove(Moves.SUPERPOWER, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, 100, 0, 3) + new AttackMove(Moves.SUPERPOWER, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3) .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF ], -1, true), new SelfStatusMove(Moves.MAGIC_COAT, Type.PSYCHIC, -1, 15, -1, 4, 3) .unimplemented(), @@ -6431,7 +6451,7 @@ export function initMoves() { .slicingMove() .windMove() .target(MoveTarget.ALL_NEAR_ENEMIES), - new AttackMove(Moves.OVERHEAT, Type.FIRE, MoveCategory.SPECIAL, 130, 90, 5, 100, 0, 3) + new AttackMove(Moves.OVERHEAT, Type.FIRE, MoveCategory.SPECIAL, 130, 90, 5, -1, 0, 3) .attr(StatChangeAttr, BattleStat.SPATK, -2, true) .attr(HealStatusEffectAttr, true, StatusEffect.FREEZE), new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3) @@ -6464,7 +6484,7 @@ export function initMoves() { new AttackMove(Moves.SKY_UPPERCUT, Type.FIGHTING, MoveCategory.PHYSICAL, 85, 90, 15, -1, 0, 3) .attr(HitsTagAttr, BattlerTagType.FLYING) .punchingMove(), - new AttackMove(Moves.SAND_TOMB, Type.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, 100, 0, 3) + new AttackMove(Moves.SAND_TOMB, Type.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3) .attr(TrapAttr, BattlerTagType.SAND_TOMB) .makesContact(false), new AttackMove(Moves.SHEER_COLD, Type.ICE, MoveCategory.SPECIAL, 200, 20, 5, -1, 0, 3) @@ -6534,7 +6554,7 @@ export function initMoves() { .pulseMove(), new AttackMove(Moves.DOOM_DESIRE, Type.STEEL, MoveCategory.SPECIAL, 140, 100, 5, -1, 0, 3) .attr(DelayedAttackAttr, ArenaTagType.DOOM_DESIRE, ChargeAnim.DOOM_DESIRE_CHARGING, "chose\nDoom Desire as its destiny!"), - new AttackMove(Moves.PSYCHO_BOOST, Type.PSYCHIC, MoveCategory.SPECIAL, 140, 90, 5, 100, 0, 3) + new AttackMove(Moves.PSYCHO_BOOST, Type.PSYCHIC, MoveCategory.SPECIAL, 140, 90, 5, -1, 0, 3) .attr(StatChangeAttr, BattleStat.SPATK, -2, true), new SelfStatusMove(Moves.ROOST, Type.FLYING, -1, 5, -1, 0, 4) .attr(HealAttr, 0.5) @@ -6548,7 +6568,7 @@ export function initMoves() { new AttackMove(Moves.WAKE_UP_SLAP, Type.FIGHTING, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 4) .attr(MovePowerMultiplierAttr, (user, target, move) => targetSleptOrComatoseCondition(user, target, move) ? 2 : 1) .attr(HealStatusEffectAttr, false, StatusEffect.SLEEP), - new AttackMove(Moves.HAMMER_ARM, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 90, 10, 100, 0, 4) + new AttackMove(Moves.HAMMER_ARM, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 90, 10, -1, 0, 4) .attr(StatChangeAttr, BattleStat.SPD, -1, true) .punchingMove(), new AttackMove(Moves.GYRO_BALL, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 4) @@ -6582,7 +6602,7 @@ export function initMoves() { .target(MoveTarget.ATTACKER), new AttackMove(Moves.U_TURN, Type.BUG, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 4) .attr(ForceSwitchOutAttr, true, false), - new AttackMove(Moves.CLOSE_COMBAT, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, 100, 0, 4) + new AttackMove(Moves.CLOSE_COMBAT, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4) .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], -1, true), new AttackMove(Moves.PAYBACK, Type.DARK, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 4) .attr(MovePowerMultiplierAttr, (user, target, move) => target.getLastXMoves(1).find(m => m.turn === target.scene.currentBattle.turn) || user.scene.currentBattle.turnCommands[target.getBattlerIndex()].command === Command.BALL ? 2 : 1), @@ -6626,9 +6646,9 @@ export function initMoves() { new SelfStatusMove(Moves.COPYCAT, Type.NORMAL, -1, 20, -1, 0, 4) .attr(CopyMoveAttr) .ignoresVirtual(), - new StatusMove(Moves.POWER_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 4) + new StatusMove(Moves.POWER_SWAP, Type.PSYCHIC, -1, 10, 100, 0, 4) .unimplemented(), - new StatusMove(Moves.GUARD_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 4) + new StatusMove(Moves.GUARD_SWAP, Type.PSYCHIC, -1, 10, 100, 0, 4) .unimplemented(), new AttackMove(Moves.PUNISHMENT, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 4) .makesContact(true) @@ -6756,7 +6776,7 @@ export function initMoves() { .attr(AddArenaTagAttr, ArenaTagType.TRICK_ROOM, 5) .ignoresProtect() .target(MoveTarget.BOTH_SIDES), - new AttackMove(Moves.DRACO_METEOR, Type.DRAGON, MoveCategory.SPECIAL, 130, 90, 5, 100, 0, 4) + new AttackMove(Moves.DRACO_METEOR, Type.DRAGON, MoveCategory.SPECIAL, 130, 90, 5, -1, 0, 4) .attr(StatChangeAttr, BattleStat.SPATK, -2, true), new AttackMove(Moves.DISCHARGE, Type.ELECTRIC, MoveCategory.SPECIAL, 80, 100, 15, 30, 0, 4) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) @@ -6764,7 +6784,7 @@ export function initMoves() { new AttackMove(Moves.LAVA_PLUME, Type.FIRE, MoveCategory.SPECIAL, 80, 100, 15, 30, 0, 4) .attr(StatusEffectAttr, StatusEffect.BURN) .target(MoveTarget.ALL_NEAR_OTHERS), - new AttackMove(Moves.LEAF_STORM, Type.GRASS, MoveCategory.SPECIAL, 130, 90, 5, 100, 0, 4) + new AttackMove(Moves.LEAF_STORM, Type.GRASS, MoveCategory.SPECIAL, 130, 90, 5, -1, 0, 4) .attr(StatChangeAttr, BattleStat.SPATK, -2, true), new AttackMove(Moves.POWER_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 120, 85, 10, -1, 0, 4), new AttackMove(Moves.ROCK_WRECKER, Type.ROCK, MoveCategory.PHYSICAL, 150, 90, 5, -1, 0, 4) @@ -6834,7 +6854,7 @@ export function initMoves() { .unimplemented(), new AttackMove(Moves.CRUSH_GRIP, Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 4) .attr(OpponentHighHpPowerAttr), - new AttackMove(Moves.MAGMA_STORM, Type.FIRE, MoveCategory.SPECIAL, 100, 75, 5, 100, 0, 4) + new AttackMove(Moves.MAGMA_STORM, Type.FIRE, MoveCategory.SPECIAL, 100, 75, 5, -1, 0, 4) .attr(TrapAttr, BattlerTagType.MAGMA_STORM), new StatusMove(Moves.DARK_VOID, Type.DARK, 50, 10, -1, 0, 4) .attr(StatusEffectAttr, StatusEffect.SLEEP) @@ -7094,7 +7114,7 @@ export function initMoves() { new AttackMove(Moves.ICICLE_CRASH, Type.ICE, MoveCategory.PHYSICAL, 85, 90, 10, 30, 0, 5) .attr(FlinchAttr) .makesContact(false), - new AttackMove(Moves.V_CREATE, Type.FIRE, MoveCategory.PHYSICAL, 180, 95, 5, 100, 0, 5) + new AttackMove(Moves.V_CREATE, Type.FIRE, MoveCategory.PHYSICAL, 180, 95, 5, -1, 0, 5) .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF, BattleStat.SPD ], -1, true), new AttackMove(Moves.FUSION_FLARE, Type.FIRE, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 5) .attr(HealStatusEffectAttr, true, StatusEffect.FREEZE) @@ -7113,7 +7133,7 @@ export function initMoves() { .condition(new FirstMoveCondition()), new AttackMove(Moves.BELCH, Type.POISON, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 6) .condition((user, target, move) => user.battleData.berriesEaten.length > 0), - new StatusMove(Moves.ROTOTILLER, Type.GROUND, -1, 10, 100, 0, 6) + new StatusMove(Moves.ROTOTILLER, Type.GROUND, -1, 10, -1, 0, 6) .target(MoveTarget.ALL) .condition((user,target,move) => { // If any fielded pokémon is grass-type and grounded. @@ -7132,7 +7152,7 @@ export function initMoves() { new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6) .attr(AddTypeAttr, Type.GHOST) .partial(), - new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, 100, 0, 6) + new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6) .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], -1) .soundBased(), new StatusMove(Moves.ION_DELUGE, Type.ELECTRIC, -1, 25, -1, 1, 6) @@ -7155,7 +7175,7 @@ export function initMoves() { new AttackMove(Moves.DISARMING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 40, -1, 15, -1, 0, 6) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), - new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, 100, 0, 6) + new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6) .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], -1, false, null, true, true, MoveEffectTrigger.PRE_APPLY) .attr(ForceSwitchOutAttr, true, false) .soundBased(), @@ -7168,7 +7188,7 @@ export function initMoves() { new StatusMove(Moves.CRAFTY_SHIELD, Type.FAIRY, -1, 10, -1, 3, 6) .target(MoveTarget.USER_SIDE) .attr(AddArenaTagAttr, ArenaTagType.CRAFTY_SHIELD, 1, true, true), - new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, 100, 0, 6) + new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6) .target(MoveTarget.ALL) .unimplemented(), new StatusMove(Moves.GRASSY_TERRAIN, Type.GRASS, -1, 10, -1, 0, 6) @@ -7193,9 +7213,9 @@ export function initMoves() { .unimplemented(), new SelfStatusMove(Moves.KINGS_SHIELD, Type.STEEL, -1, 10, -1, 4, 6) .attr(ProtectAttr, BattlerTagType.KINGS_SHIELD), - new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, 100, 0, 6) + new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6) .attr(StatChangeAttr, BattleStat.ATK, -1), - new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, 100, 0, 6) + new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6) .attr(StatChangeAttr, BattleStat.SPATK, -1) .soundBased(), new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6) @@ -7221,7 +7241,7 @@ export function initMoves() { .target(MoveTarget.NEAR_ALLY), new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) .attr(StatChangeAttr, BattleStat.SPATK, -2), - new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, 100, 0, 6) + new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6) .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK, BattleStat.SPD ], -1, false, (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC) .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6) @@ -7252,7 +7272,7 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.PARALYSIS), new AttackMove(Moves.HOLD_BACK, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 6) .attr(SurviveDamageAttr), - new AttackMove(Moves.INFESTATION, Type.BUG, MoveCategory.SPECIAL, 20, 100, 20, 100, 0, 6) + new AttackMove(Moves.INFESTATION, Type.BUG, MoveCategory.SPECIAL, 20, 100, 20, -1, 0, 6) .makesContact() .attr(TrapAttr, BattlerTagType.INFESTATION), new AttackMove(Moves.POWER_UP_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 20, 100, 0, 6) @@ -7261,7 +7281,7 @@ export function initMoves() { new AttackMove(Moves.OBLIVION_WING, Type.FLYING, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 6) .attr(HitHealAttr, 0.75) .triageMove(), - new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, 100, 0, 6) + new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) .attr(NeutralDamageAgainstFlyingTypeMultiplierAttr) .attr(HitsTagAttr, BattlerTagType.FLYING, false) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) @@ -7284,9 +7304,9 @@ export function initMoves() { new AttackMove(Moves.PRECIPICE_BLADES, Type.GROUND, MoveCategory.PHYSICAL, 120, 85, 10, -1, 0, 6) .makesContact(false) .target(MoveTarget.ALL_NEAR_ENEMIES), - new AttackMove(Moves.DRAGON_ASCENT, Type.FLYING, MoveCategory.PHYSICAL, 120, 100, 5, 100, 0, 6) + new AttackMove(Moves.DRAGON_ASCENT, Type.FLYING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 6) .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], -1, true), - new AttackMove(Moves.HYPERSPACE_FURY, Type.DARK, MoveCategory.PHYSICAL, 100, -1, 5, 100, 0, 6) + new AttackMove(Moves.HYPERSPACE_FURY, Type.DARK, MoveCategory.PHYSICAL, 100, -1, 5, -1, 0, 6) .attr(StatChangeAttr, BattleStat.DEF, -1, true) .makesContact(false) .ignoresProtect(), @@ -7410,23 +7430,23 @@ export function initMoves() { .condition(new FirstMoveCondition()), new SelfStatusMove(Moves.BANEFUL_BUNKER, Type.POISON, -1, 10, -1, 4, 7) .attr(ProtectAttr, BattlerTagType.BANEFUL_BUNKER), - new AttackMove(Moves.SPIRIT_SHACKLE, Type.GHOST, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 7) + new AttackMove(Moves.SPIRIT_SHACKLE, Type.GHOST, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 7) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1) .makesContact(false), new AttackMove(Moves.DARKEST_LARIAT, Type.DARK, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 7) .attr(IgnoreOpponentStatChangesAttr), - new AttackMove(Moves.SPARKLING_ARIA, Type.WATER, MoveCategory.SPECIAL, 90, 100, 10, -1, 0, 7) + new AttackMove(Moves.SPARKLING_ARIA, Type.WATER, MoveCategory.SPECIAL, 90, 100, 10, 100, 0, 7) .attr(HealStatusEffectAttr, false, StatusEffect.BURN) .soundBased() .target(MoveTarget.ALL_NEAR_OTHERS), - new AttackMove(Moves.ICE_HAMMER, Type.ICE, MoveCategory.PHYSICAL, 100, 90, 10, 100, 0, 7) + new AttackMove(Moves.ICE_HAMMER, Type.ICE, MoveCategory.PHYSICAL, 100, 90, 10, -1, 0, 7) .attr(StatChangeAttr, BattleStat.SPD, -1, true) .punchingMove(), new StatusMove(Moves.FLORAL_HEALING, Type.FAIRY, -1, 10, -1, 0, 7) .attr(BoostHealAttr, 0.5, 2/3, true, false, (user, target, move) => user.scene.arena.terrain?.terrainType === TerrainType.GRASSY) .triageMove(), new AttackMove(Moves.HIGH_HORSEPOWER, Type.GROUND, MoveCategory.PHYSICAL, 95, 95, 10, -1, 0, 7), - new StatusMove(Moves.STRENGTH_SAP, Type.GRASS, 100, 10, 100, 0, 7) + new StatusMove(Moves.STRENGTH_SAP, Type.GRASS, 100, 10, -1, 0, 7) .attr(HitHealAttr, null, Stat.ATK) .attr(StatChangeAttr, BattleStat.ATK, -1) .condition((user, target, move) => target.summonData.battleStats[BattleStat.ATK] > -6) @@ -7439,7 +7459,7 @@ export function initMoves() { .makesContact(false), new StatusMove(Moves.SPOTLIGHT, Type.NORMAL, -1, 15, -1, 3, 7) .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false), - new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, 100, 0, 7) + new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, -1, 0, 7) .attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatChangeAttr, BattleStat.SPD, -1), new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7) @@ -7454,7 +7474,7 @@ export function initMoves() { .attr(StatusCategoryOnAllyAttr) .attr(HealOnAllyAttr, 0.5, true, false) .ballBombMove(), - new AttackMove(Moves.ANCHOR_SHOT, Type.STEEL, MoveCategory.PHYSICAL, 80, 100, 20, -1, 0, 7) + new AttackMove(Moves.ANCHOR_SHOT, Type.STEEL, MoveCategory.PHYSICAL, 80, 100, 20, 100, 0, 7) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1), new StatusMove(Moves.PSYCHIC_TERRAIN, Type.PSYCHIC, -1, 10, -1, 0, 7) .attr(TerrainChangeAttr, TerrainType.PSYCHIC) @@ -7498,7 +7518,7 @@ export function initMoves() { .ballBombMove() .makesContact(false) .partial(), - new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, 100, 0, 7) + new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, -1, 0, 7) .attr(StatChangeAttr, BattleStat.DEF, -1, true) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -7535,14 +7555,14 @@ export function initMoves() { new SelfStatusMove(Moves.EXTREME_EVOBOOST, Type.NORMAL, -1, 1, 100, 0, 7) .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 2, true) .ignoresVirtual(), - new AttackMove(Moves.GENESIS_SUPERNOVA, Type.PSYCHIC, MoveCategory.SPECIAL, 185, -1, 1, -1, 0, 7) + new AttackMove(Moves.GENESIS_SUPERNOVA, Type.PSYCHIC, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7) .attr(TerrainChangeAttr, TerrainType.PSYCHIC) .ignoresVirtual(), /* End Unused */ new AttackMove(Moves.SHELL_TRAP, Type.FIRE, MoveCategory.SPECIAL, 150, 100, 5, -1, -3, 7) .target(MoveTarget.ALL_NEAR_ENEMIES) .partial(), - new AttackMove(Moves.FLEUR_CANNON, Type.FAIRY, MoveCategory.SPECIAL, 130, 90, 5, 100, 0, 7) + new AttackMove(Moves.FLEUR_CANNON, Type.FAIRY, MoveCategory.SPECIAL, 130, 90, 5, -1, 0, 7) .attr(StatChangeAttr, BattleStat.SPATK, -2, true), new AttackMove(Moves.PSYCHIC_FANGS, Type.PSYCHIC, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 7) .bitingMove() @@ -7565,7 +7585,7 @@ export function initMoves() { new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) .ignoresAbilities() .partial(), - new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, 100, 0, 7) + new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7) .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], -1), new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7) .attr(FlinchAttr), @@ -7675,7 +7695,7 @@ export function initMoves() { new SelfStatusMove(Moves.NO_RETREAT, Type.FIGHTING, -1, 5, 100, 0, 8) .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, true, true, 1), - new StatusMove(Moves.TAR_SHOT, Type.ROCK, 100, 15, 100, 0, 8) + new StatusMove(Moves.TAR_SHOT, Type.ROCK, 100, 15, -1, 0, 8) .attr(StatChangeAttr, BattleStat.SPD, -1) .partial(), new StatusMove(Moves.MAGIC_POWDER, Type.PSYCHIC, 100, 20, -1, 0, 8) @@ -7777,12 +7797,12 @@ export function initMoves() { .danceMove(), new AttackMove(Moves.BODY_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 8) .attr(DefAtkAttr), - new StatusMove(Moves.DECORATE, Type.FAIRY, -1, 15, 100, 0, 8) + new StatusMove(Moves.DECORATE, Type.FAIRY, -1, 15, -1, 0, 8) .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], 2), new AttackMove(Moves.DRUM_BEATING, Type.GRASS, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 8) .attr(StatChangeAttr, BattleStat.SPD, -1) .makesContact(false), - new AttackMove(Moves.SNAP_TRAP, Type.GRASS, MoveCategory.PHYSICAL, 35, 100, 15, 100, 0, 8) + new AttackMove(Moves.SNAP_TRAP, Type.GRASS, MoveCategory.PHYSICAL, 35, 100, 15, -1, 0, 8) .attr(TrapAttr, BattlerTagType.SNAP_TRAP), new AttackMove(Moves.PYRO_BALL, Type.FIRE, MoveCategory.PHYSICAL, 120, 90, 5, 10, 0, 8) .attr(HealStatusEffectAttr, true, StatusEffect.FREEZE) @@ -7834,7 +7854,7 @@ export function initMoves() { new AttackMove(Moves.STEEL_ROLLER, Type.STEEL, MoveCategory.PHYSICAL, 130, 100, 5, -1, 0, 8) .attr(ClearTerrainAttr) .condition((user, target, move) => !!user.scene.arena.terrain), - new AttackMove(Moves.SCALE_SHOT, Type.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, 100, 0, 8) + new AttackMove(Moves.SCALE_SHOT, Type.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8) //.attr(StatChangeAttr, BattleStat.SPD, 1, true) // TODO: Have boosts only apply at end of move, not after every hit //.attr(StatChangeAttr, BattleStat.DEF, -1, true) .attr(MultiHitAttr) @@ -7875,7 +7895,7 @@ export function initMoves() { new StatusMove(Moves.CORROSIVE_GAS, Type.POISON, 100, 40, -1, 0, 8) .target(MoveTarget.ALL_NEAR_OTHERS) .unimplemented(), - new StatusMove(Moves.COACHING, Type.FIGHTING, -1, 10, 100, 0, 8) + new StatusMove(Moves.COACHING, Type.FIGHTING, -1, 10, -1, 0, 8) .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF ], 1) .target(MoveTarget.NEAR_ALLY), new AttackMove(Moves.FLIP_TURN, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 20, -1, 0, 8) @@ -7901,7 +7921,7 @@ export function initMoves() { .attr(MultiHitAttr, MultiHitType._3) .attr(CritOnlyAttr) .punchingMove(), - new AttackMove(Moves.THUNDER_CAGE, Type.ELECTRIC, MoveCategory.SPECIAL, 80, 90, 15, 100, 0, 8) + new AttackMove(Moves.THUNDER_CAGE, Type.ELECTRIC, MoveCategory.SPECIAL, 80, 90, 15, -1, 0, 8) .attr(TrapAttr, BattlerTagType.THUNDER_CAGE), new AttackMove(Moves.DRAGON_ENERGY, Type.DRAGON, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 8) .attr(HpPowerAttr) @@ -7950,7 +7970,7 @@ export function initMoves() { new SelfStatusMove(Moves.VICTORY_DANCE, Type.FIGHTING, -1, 10, 100, 0, 8) .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPD ], 1, true) .danceMove(), - new AttackMove(Moves.HEADLONG_RUSH, Type.GROUND, MoveCategory.PHYSICAL, 120, 100, 5, 100, 0, 8) + new AttackMove(Moves.HEADLONG_RUSH, Type.GROUND, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 8) .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], -1, true) .punchingMove(), new AttackMove(Moves.BARB_BARRAGE, Type.POISON, MoveCategory.PHYSICAL, 60, 100, 10, 50, 0, 8) @@ -8113,15 +8133,15 @@ export function initMoves() { .makesContact(false), new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9) .attr(StatChangeAttr, BattleStat.SPDEF, -2), - new AttackMove(Moves.ORDER_UP, Type.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 9) + new AttackMove(Moves.ORDER_UP, Type.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 9) .makesContact(false) .partial(), new AttackMove(Moves.JET_PUNCH, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 15, -1, 1, 9) .punchingMove(), - new StatusMove(Moves.SPICY_EXTRACT, Type.GRASS, -1, 15, 100, 0, 9) + new StatusMove(Moves.SPICY_EXTRACT, Type.GRASS, -1, 15, -1, 0, 9) .attr(StatChangeAttr, BattleStat.ATK, 2) .attr(StatChangeAttr, BattleStat.DEF, -2), - new AttackMove(Moves.SPIN_OUT, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, 100, 0, 9) + new AttackMove(Moves.SPIN_OUT, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 9) .attr(StatChangeAttr, BattleStat.SPD, -2, true), new AttackMove(Moves.POPULATION_BOMB, Type.NORMAL, MoveCategory.PHYSICAL, 20, 90, 10, -1, 0, 9) .attr(MultiHitAttr, MultiHitType._10) @@ -8135,7 +8155,7 @@ export function initMoves() { .triageMove() .attr(RevivalBlessingAttr) .target(MoveTarget.USER), - new AttackMove(Moves.SALT_CURE, Type.ROCK, MoveCategory.PHYSICAL, 40, 100, 15, -1, 0, 9) + new AttackMove(Moves.SALT_CURE, Type.ROCK, MoveCategory.PHYSICAL, 40, 100, 15, 100, 0, 9) .attr(AddBattlerTagAttr, BattlerTagType.SALT_CURED) .makesContact(false), new AttackMove(Moves.TRIPLE_DIVE, Type.WATER, MoveCategory.PHYSICAL, 30, 95, 10, -1, 0, 9) @@ -8209,7 +8229,7 @@ export function initMoves() { .attr(StatChangeAttr, BattleStat.SPD, -1), new AttackMove(Moves.TRAILBLAZE, Type.GRASS, MoveCategory.PHYSICAL, 50, 100, 20, 100, 0, 9) .attr(StatChangeAttr, BattleStat.SPD, 1, true), - new AttackMove(Moves.CHILLING_WATER, Type.WATER, MoveCategory.SPECIAL, 50, 100, 20, -1, 0, 9) + new AttackMove(Moves.CHILLING_WATER, Type.WATER, MoveCategory.SPECIAL, 50, 100, 20, 100, 0, 9) .attr(StatChangeAttr, BattleStat.ATK, -1), new AttackMove(Moves.HYPER_DRILL, Type.NORMAL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 9) .ignoresProtect(), @@ -8302,7 +8322,7 @@ export function initMoves() { .slicingMove(), new AttackMove(Moves.HARD_PRESS, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 9) .attr(OpponentHighHpPowerAttr), - new StatusMove(Moves.DRAGON_CHEER, Type.DRAGON, -1, 15, 100, 0, 9) + new StatusMove(Moves.DRAGON_CHEER, Type.DRAGON, -1, 15, -1, 0, 9) .attr(AddBattlerTagAttr, BattlerTagType.CRIT_BOOST, false, true) .target(MoveTarget.NEAR_ALLY) .partial(), diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 5ea983fb598..181cf6744c0 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -24,6 +24,9 @@ import * as Overrides from "../overrides"; import { ModifierType, modifierTypes } from "./modifier-type"; import { Command } from "#app/ui/command-ui-handler.js"; +import { allMoves } from "#app/data/move.js"; +import { Abilities } from "#app/enums/abilities.js"; + export type ModifierPredicate = (modifier: Modifier) => boolean; const iconOverflowIndex = 24; @@ -520,6 +523,18 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { return 1; } + //Applies to items with chance of activating secondary effects ie Kings Rock + getSecondaryChanceMultiplier(pokemon: Pokemon): integer { + const sheerForceAffected = allMoves[pokemon.getLastXMoves(0)[0].move].chance >= 0 && pokemon.hasAbility(Abilities.SHEER_FORCE); + + if (sheerForceAffected) { + return 0; + } else if (pokemon.hasAbility(Abilities.SERENE_GRACE)) { + return 2; + } + return 1; + } + getMaxStackCount(scene: BattleScene, forThreshold?: boolean): integer { const pokemon = this.getPokemon(scene); if (!pokemon) { @@ -831,7 +846,7 @@ export class FlinchChanceModifier extends PokemonHeldItemModifier { const pokemon = args[0] as Pokemon; const flinched = args[1] as Utils.BooleanHolder; - if (!flinched.value && pokemon.randSeedInt(10) < this.getStackCount()) { + if (!flinched.value && pokemon.randSeedInt(10) < (this.getStackCount() * this.getSecondaryChanceMultiplier(pokemon))) { flinched.value = true; return true; } diff --git a/src/phases.ts b/src/phases.ts index fd4592d3ff9..dd3ebf148fb 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1,7 +1,7 @@ import BattleScene, { bypassLogin } from "./battle-scene"; import { default as Pokemon, PlayerPokemon, EnemyPokemon, PokemonMove, MoveResult, DamageResult, FieldPosition, HitResult, TurnMove } from "./field/pokemon"; import * as Utils from "./utils"; -import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, VariableAccuracyAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, IgnoreOpponentStatChangesAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, OneHitKOAccuracyAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr } from "./data/move"; +import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, VariableAccuracyAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, IgnoreOpponentStatChangesAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, OneHitKOAccuracyAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr } from "./data/move"; import { Mode } from "./ui/ui"; import { Command } from "./ui/command-ui-handler"; import { Stat } from "./data/pokemon-stat"; @@ -26,7 +26,7 @@ import { Gender } from "./data/gender"; import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather"; import { TempBattleStat } from "./data/temp-battle-stat"; import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag"; -import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, WonderSkinAbAttr, applyPreDefendAbAttrs } from "./data/ability"; +import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, WonderSkinAbAttr, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./field/arena"; import { BattleType, BattlerIndex, TurnCommand } from "./battle"; @@ -646,7 +646,7 @@ export class BattlePhase extends Phase { const tintSprites = this.scene.currentBattle.trainer.getTintSprites(); for (let i = 0; i < sprites.length; i++) { const visible = !trainerSlot || !i === (trainerSlot === TrainerSlot.TRAINER) || sprites.length < 2; - [ sprites[i], tintSprites[i] ].map(sprite => { + [sprites[i], tintSprites[i]].map(sprite => { if (visible) { sprite.x = trainerSlot || sprites.length < 2 ? 0 : i ? 16 : -16; } @@ -943,7 +943,7 @@ export class EncounterPhase extends BattlePhase { const enemyField = this.scene.getEnemyField(); this.scene.tweens.add({ - targets: [ this.scene.arenaEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.arenaPlayer, this.scene.trainer ].flat(), + targets: [this.scene.arenaEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.arenaPlayer, this.scene.trainer].flat(), x: (_target, _key, value, fieldIndex: integer) => fieldIndex < 2 + (enemyField.length) ? value + 300 : value - 300, duration: 2000, onComplete: () => { @@ -958,21 +958,21 @@ export class EncounterPhase extends BattlePhase { const enemyField = this.scene.getEnemyField(); if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { - return i18next.t("battle:bossAppeared", {bossName: enemyField[0].name}); + return i18next.t("battle:bossAppeared", { bossName: enemyField[0].name }); } if (this.scene.currentBattle.battleType === BattleType.TRAINER) { if (this.scene.currentBattle.double) { - return i18next.t("battle:trainerAppearedDouble", {trainerName: this.scene.currentBattle.trainer.getName(TrainerSlot.NONE, true)}); + return i18next.t("battle:trainerAppearedDouble", { trainerName: this.scene.currentBattle.trainer.getName(TrainerSlot.NONE, true) }); } else { - return i18next.t("battle:trainerAppeared", {trainerName: this.scene.currentBattle.trainer.getName(TrainerSlot.NONE, true)}); + return i18next.t("battle:trainerAppeared", { trainerName: this.scene.currentBattle.trainer.getName(TrainerSlot.NONE, true) }); } } return enemyField.length === 1 - ? i18next.t("battle:singleWildAppeared", {pokemonName: enemyField[0].name}) - : i18next.t("battle:multiWildAppeared", {pokemonName1: enemyField[0].name, pokemonName2: enemyField[1].name}); + ? i18next.t("battle:singleWildAppeared", { pokemonName: enemyField[0].name }) + : i18next.t("battle:multiWildAppeared", { pokemonName1: enemyField[0].name, pokemonName2: enemyField[1].name }); } doEncounterCommon(showEncounterMessage: boolean = true) { @@ -1002,7 +1002,7 @@ export class EncounterPhase extends BattlePhase { this.scene.currentBattle.started = true; this.scene.playBgm(undefined); this.scene.pbTray.showPbTray(this.scene.getParty()); - this.scene.pbTrayEnemy.showPbTray(this.scene.getEnemyParty()); + this.scene.pbTrayEnemy.showPbTray(this.scene.getEnemyParty()); const doTrainerSummon = () => { this.hideEnemyTrainer(); const availablePartyMembers = this.scene.getEnemyParty().filter(p => !p.isFainted()).length; @@ -1028,7 +1028,7 @@ export class EncounterPhase extends BattlePhase { this.scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.waveIndex); const showDialogueAndSummon = () => { - this.scene.ui.showDialogue(message, trainer.getName(TrainerSlot.NONE,true), null, () => { + this.scene.ui.showDialogue(message, trainer.getName(TrainerSlot.NONE, true), null, () => { this.scene.charSprite.hide().then(() => this.scene.hideFieldOverlay(250).then(() => doSummon())); }); }; @@ -1059,7 +1059,7 @@ export class EncounterPhase extends BattlePhase { // how many player pokemon are on the field ? const pokemonsOnFieldCount = this.scene.getParty().filter(p => p.isOnField()).length; // if it's a 2vs1, there will never be a 2nd pokemon on our field even - const requiredPokemonsOnField = Math.min(this.scene.getParty().filter((p) => !p.isFainted()).length, 2); + const requiredPokemonsOnField = Math.min(this.scene.getParty().filter((p) => !p.isFainted()).length, 2); // if it's a double, there should be 2, otherwise 1 if (this.scene.currentBattle.double) { return pokemonsOnFieldCount === requiredPokemonsOnField; @@ -1145,7 +1145,7 @@ export class NextEncounterPhase extends EncounterPhase { const enemyField = this.scene.getEnemyField(); this.scene.tweens.add({ - targets: [ this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.lastEnemyTrainer ].flat(), + targets: [this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.lastEnemyTrainer].flat(), x: "+=300", duration: 2000, onComplete: () => { @@ -1188,7 +1188,7 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { const enemyField = this.scene.getEnemyField(); this.scene.tweens.add({ - targets: [ this.scene.arenaEnemy, enemyField ].flat(), + targets: [this.scene.arenaEnemy, enemyField].flat(), x: "+=300", duration: 2000, onComplete: () => { @@ -1254,7 +1254,7 @@ export class SelectBiomePhase extends BattlePhase { let biomeChoices: Biome[]; this.scene.executeWithSeedOffset(() => { biomeChoices = (!Array.isArray(biomeLinks[currentBiome]) - ? [ biomeLinks[currentBiome] as Biome ] + ? [biomeLinks[currentBiome] as Biome] : biomeLinks[currentBiome] as (Biome | [Biome, integer])[]) .filter((b, i) => !Array.isArray(b) || !Utils.randSeedInt(b[1])) .map(b => Array.isArray(b) ? b[0] : b); @@ -1309,7 +1309,7 @@ export class SwitchBiomePhase extends BattlePhase { } this.scene.tweens.add({ - targets: [ this.scene.arenaEnemy, this.scene.lastEnemyTrainer ], + targets: [this.scene.arenaEnemy, this.scene.lastEnemyTrainer], x: "+=300", duration: 2000, onComplete: () => { @@ -1327,7 +1327,7 @@ export class SwitchBiomePhase extends BattlePhase { this.scene.arenaPlayerTransition.setVisible(true); this.scene.tweens.add({ - targets: [ this.scene.arenaPlayer, this.scene.arenaBgTransition, this.scene.arenaPlayerTransition ], + targets: [this.scene.arenaPlayer, this.scene.arenaBgTransition, this.scene.arenaPlayerTransition], duration: 1000, delay: 1000, ease: "Sine.easeInOut", @@ -1807,7 +1807,7 @@ export class SummonMissingPhase extends SummonPhase { } preSummon(): void { - this.scene.ui.showText(i18next.t("battle:sendOutPokemon", { pokemonName: this.getPokemon().name})); + this.scene.ui.showText(i18next.t("battle:sendOutPokemon", { pokemonName: this.getPokemon().name })); this.scene.time.delayedCall(250, () => this.summon()); } } @@ -1839,7 +1839,7 @@ export class TurnInitPhase extends FieldPhase { this.scene.getPlayerField().forEach(p => { // If this pokemon is in play and evolved into something illegal under the current challenge, force a switch if (p.isOnField() && !p.isAllowedInBattle()) { - this.scene.queueMessage(i18next.t("challenges:illegalEvolution", {"pokemon": p.name}), null, true); + this.scene.queueMessage(i18next.t("challenges:illegalEvolution", { "pokemon": p.name }), null, true); const allowedPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle()); @@ -1920,7 +1920,7 @@ export class CommandPhase extends FieldPhase { while (moveQueue.length && moveQueue[0] && moveQueue[0].move && (!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move) - || !playerPokemon.getMoveset()[playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable(playerPokemon, moveQueue[0].ignorePP))) { + || !playerPokemon.getMoveset()[playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable(playerPokemon, moveQueue[0].ignorePP))) { moveQueue.shift(); } @@ -1950,13 +1950,13 @@ export class CommandPhase extends FieldPhase { case Command.FIGHT: let useStruggle = false; if (cursor === -1 || - playerPokemon.trySelectMove(cursor, args[0] as boolean) || - (useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m.isUsable(playerPokemon)).length)) { + playerPokemon.trySelectMove(cursor, args[0] as boolean) || + (useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m.isUsable(playerPokemon)).length)) { const moveId = !useStruggle ? cursor > -1 ? playerPokemon.getMoveset()[cursor].moveId : Moves.NONE : Moves.STRUGGLE; const turnCommand: TurnCommand = { command: Command.FIGHT, cursor: cursor, move: { move: moveId, targets: [], ignorePP: args[0] }, args: args }; const moveTargets: MoveTargetSet = args.length < 3 ? getMoveTargets(playerPokemon, moveId) : args[2]; if (!moveId) { - turnCommand.targets = [ this.fieldIndex ]; + turnCommand.targets = [this.fieldIndex]; } console.log(moveTargets, playerPokemon.name); if (moveTargets.targets.length <= 1 || moveTargets.multiple) { @@ -2177,7 +2177,7 @@ export class EnemyCommandPhase extends FieldPhase { const index = trainer.getNextSummonIndex(enemyPokemon.trainerSlot, partyMemberScores); battle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] = - { command: Command.POKEMON, cursor: index, args: [ false ] }; + { command: Command.POKEMON, cursor: index, args: [false] }; battle.enemySwitchCounter++; @@ -2214,7 +2214,7 @@ export class SelectTargetPhase extends PokemonPhase { this.scene.currentBattle.turnCommands[this.fieldIndex] = null; this.scene.unshiftPhase(new CommandPhase(this.scene, this.fieldIndex)); } else { - turnCommand.targets = [ cursor ]; + turnCommand.targets = [cursor]; } if (turnCommand.command === Command.BALL && this.fieldIndex) { this.scene.currentBattle.turnCommands[this.fieldIndex - 1].skip = true; @@ -2262,10 +2262,10 @@ export class TurnStartPhase extends FieldPhase { const aPriority = new Utils.IntegerHolder(aMove.priority); const bPriority = new Utils.IntegerHolder(bMove.priority); - applyMoveAttrs(IncrementMovePriorityAttr,this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a),null,aMove,aPriority); - applyMoveAttrs(IncrementMovePriorityAttr,this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b),null,bMove,bPriority); + applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a), null, aMove, aPriority); + applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b), null, bMove, bPriority); - applyAbAttrs(IncrementMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a), null, aMove, aPriority); + applyAbAttrs(IncrementMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a), null, aMove, aPriority); applyAbAttrs(IncrementMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b), null, bMove, bPriority); if (aPriority.value !== bPriority.value) { @@ -2326,7 +2326,7 @@ export class TurnStartPhase extends FieldPhase { return; } }); - // if only one pokemon is alive, use that one + // if only one pokemon is alive, use that one if (playerActivePokemon.length > 1) { // find which active pokemon has faster speed const fasterPokemon = playerActivePokemon[0].getStat(Stat.SPD) > playerActivePokemon[1].getStat(Stat.SPD) ? playerActivePokemon[0] : playerActivePokemon[1]; @@ -2619,7 +2619,7 @@ export class MovePhase extends BattlePhase { this.scene.unshiftPhase(new ShowAbilityPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr))); } moveTarget.value = oldTarget; - } + } this.targets[0] = moveTarget.value; } @@ -2846,7 +2846,7 @@ export class MoveEffectPhase extends PokemonPhase { // of the left Pokemon and gets hit unless this is checked. if (targets.includes(battlerIndex) && this.move.getMove().moveTarget === MoveTarget.ALL_NEAR_OTHERS) { const i = targets.indexOf(battlerIndex); - targets.splice(i,i+1); + targets.splice(i, i + 1); } this.targets = targets; } @@ -2886,7 +2886,7 @@ export class MoveEffectPhase extends PokemonPhase { const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual }; user.pushMoveHistory(moveHistoryEntry); - const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ])); + const targetHitChecks = Object.fromEntries(targets.map(p => [p.getBattlerIndex(), this.hitCheck(p)])); const activeTargets = targets.map(t => t.isActive(true)); if (!activeTargets.length || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]])) { user.turnData.hitCount = 1; @@ -2935,26 +2935,26 @@ export class MoveEffectPhase extends PokemonPhase { const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget(), move)); // Charge attribute with charge effect takes all effect attributes and applies them to charge stage, so ignore them if this is present Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY - && attr.selfTarget && (!attr.firstHitOnly || firstHit), user, target, move)).then(() => { + && attr.selfTarget && (!attr.firstHitOnly || firstHit), user, target, move)).then(() => { if (hitResult !== HitResult.NO_EFFECT) { - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY - && !attr.selfTarget && (!attr.firstHitOnly || firstHit), user, target, move).then(() => { - if (hitResult < HitResult.NO_EFFECT) { + applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY + && !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit), user, target, this.move.getMove()).then(() => { + if (hitResult < HitResult.NO_EFFECT && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr)) { const flinched = new Utils.BooleanHolder(false); user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); if (flinched.value) { target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); } } - Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.HIT && (!attr.firstHitOnly || firstHit), - user, target, move).then(() => { - return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, move, hitResult).then(() => { - if (!user.isPlayer() && move instanceof AttackMove) { + Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT && (!attr.firstHitOnly || firstHit), + user, target, this.move.getMove()).then(() => { + return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => { + if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); } })).then(() => { - applyPostAttackAbAttrs(PostAttackAbAttr, user, target, move, hitResult).then(() => { - if (move instanceof AttackMove) { + applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { + if (this.move.getMove() instanceof AttackMove) { this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target.getFieldIndex()); } resolve(); @@ -3151,7 +3151,7 @@ export class MoveAnimTestPhase extends BattlePhase { } initMoveAnim(this.scene, moveId).then(() => { - loadMoveAnimAssets(this.scene, [ moveId ], true) + loadMoveAnimAssets(this.scene, [moveId], true) .then(() => { new MoveAnim(moveId, player ? this.scene.getPlayerPokemon() : this.scene.getEnemyPokemon(), (player !== (allMoves[moveId] instanceof SelfStatusMove) ? this.scene.getEnemyPokemon() : this.scene.getPlayerPokemon()).getBattlerIndex()).play(this.scene, () => { if (player) { @@ -3328,7 +3328,7 @@ export class StatChangePhase extends PokemonPhase { } aggregateStatChanges(random: boolean = false): void { - const isAccEva = [ BattleStat.ACC, BattleStat.EVA ].some(s => this.stats.includes(s)); + const isAccEva = [BattleStat.ACC, BattleStat.EVA].some(s => this.stats.includes(s)); let existingPhase: StatChangePhase; if (this.stats.length === 1) { while ((existingPhase = (this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex && p.stats.length === 1 @@ -3348,7 +3348,7 @@ export class StatChangePhase extends PokemonPhase { } } while ((existingPhase = (this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex && p.selfTarget === this.selfTarget - && ([ BattleStat.ACC, BattleStat.EVA ].some(s => p.stats.includes(s)) === isAccEva) + && ([BattleStat.ACC, BattleStat.EVA].some(s => p.stats.includes(s)) === isAccEva) && p.levels === this.levels && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatChangePhase))) { this.stats.push(...existingPhase.stats); if (!this.scene.tryRemovePhase(p => p === existingPhase)) { @@ -3512,7 +3512,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { break; } if (damage) { - // Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ... + // Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ... this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage, false, true)); pokemon.updateInfo(); } @@ -3962,7 +3962,7 @@ export class TrainerVictoryPhase extends BattlePhase { const trainerType = this.scene.currentBattle.trainer.config.trainerType; if (vouchers.hasOwnProperty(TrainerType[trainerType])) { if (!this.scene.validateVoucher(vouchers[TrainerType[trainerType]]) && this.scene.currentBattle.trainer.config.isBoss) { - this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [ modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM ][vouchers[TrainerType[trainerType]].voucherType])); + this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM][vouchers[TrainerType[trainerType]].voucherType])); } } @@ -4571,7 +4571,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { if (emptyMoveIndex > -1) { pokemon.setMove(emptyMoveIndex, this.moveId); initMoveAnim(this.scene, this.moveId).then(() => { - loadMoveAnimAssets(this.scene, [ this.moveId ], true) + loadMoveAnimAssets(this.scene, [this.moveId], true) .then(() => { this.scene.ui.setMode(messageMode).then(() => { this.scene.playSound("level_up_fanfare"); @@ -4944,7 +4944,7 @@ export class AttemptCapturePhase extends PokemonPhase { } }); }; - Promise.all([ pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon) ]).then(() => { + Promise.all([pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon)]).then(() => { if (this.scene.getParty().length === 6) { const promptRelease = () => { this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.name }), null, () => { @@ -5008,7 +5008,7 @@ export class AttemptRunPhase extends PokemonPhase { this.scene.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500); this.scene.tweens.add({ - targets: [ this.scene.arenaEnemy, enemyField ].flat(), + targets: [this.scene.arenaEnemy, enemyField].flat(), alpha: 0, duration: 250, ease: "Sine.easeIn", @@ -5097,7 +5097,7 @@ export class SelectModifierPhase extends BattlePhase { this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER, -1, (fromSlotIndex: integer, itemIndex: integer, itemQuantity: integer, toSlotIndex: integer) => { if (toSlotIndex !== undefined && fromSlotIndex < 6 && toSlotIndex < 6 && fromSlotIndex !== toSlotIndex && itemIndex > -1) { const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && (m as PokemonHeldItemModifier).getTransferrable(true) && (m as PokemonHeldItemModifier).pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[]; + && (m as PokemonHeldItemModifier).getTransferrable(true) && (m as PokemonHeldItemModifier).pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[]; const itemModifier = itemModifiers[itemIndex]; this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity); } else { @@ -5223,7 +5223,7 @@ export class SelectModifierPhase extends BattlePhase { getRerollCost(typeOptions: ModifierTypeOption[], lockRarities: boolean): integer { let baseValue = 0; if (lockRarities) { - const tierValues = [ 50, 125, 300, 750, 2000 ]; + const tierValues = [50, 125, 300, 750, 2000]; for (const opt of typeOptions) { baseValue += tierValues[opt.type.tier]; } @@ -5443,7 +5443,7 @@ export class TrainerMessageTestPhase extends BattlePhase { continue; } const config = trainerConfigs[type]; - [ config.encounterMessages, config.femaleEncounterMessages, config.victoryMessages, config.femaleVictoryMessages, config.defeatMessages, config.femaleDefeatMessages ] + [config.encounterMessages, config.femaleEncounterMessages, config.victoryMessages, config.femaleVictoryMessages, config.defeatMessages, config.femaleDefeatMessages] .map(messages => { if (messages?.length) { testMessages.push(...messages); diff --git a/src/test/abilities/serene_grace.test.ts b/src/test/abilities/serene_grace.test.ts new file mode 100644 index 00000000000..cf283ea92a8 --- /dev/null +++ b/src/test/abilities/serene_grace.test.ts @@ -0,0 +1,110 @@ +import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import {Abilities} from "#enums/abilities"; +import {applyAbAttrs ,MoveEffectChanceMultiplierAbAttr} from "#app/data/ability"; +import {Species} from "#enums/species"; +import { + CommandPhase, + MoveEffectPhase, +} from "#app/phases"; +import {Mode} from "#app/ui/ui"; +import {Stat} from "#app/data/pokemon-stat"; +import {Moves} from "#enums/moves"; +import {getMovePosition} from "#app/test/utils/gameManagerUtils"; +import {Command} from "#app/ui/command-ui-handler"; +import * as Utils from "#app/utils"; + + +describe("Abilities - Serene Grace", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + const movesToUse = [Moves.AIR_SLASH, Moves.TACKLE]; + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.ONIX); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue(movesToUse); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]); + }); + + it("Move chance without Serene Grace", async() => { + const moveToUse = Moves.AIR_SLASH; + await game.startBattle([ + Species.PIDGEOT + ]); + + + game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; + game.scene.getEnemyParty()[0].stats[Stat.SPD] = 1; + expect(game.scene.getParty()[0].formIndex).toBe(0); + + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + // Check chance of Air Slash without Serene Grace + const phase = game.scene.getCurrentPhase() as MoveEffectPhase; + const move = phase.move.getMove(); + expect(move.id).toBe(Moves.AIR_SLASH); + + const chance = new Utils.IntegerHolder(move.chance); + console.log(move.chance + " Their ability is " + phase.getUserPokemon().getAbility().name); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon(), null, chance, move, phase.getTarget(), false); + expect(chance.value).toBe(30); + + }, 20000); + + it("Move chance with Serene Grace", async() => { + const moveToUse = Moves.AIR_SLASH; + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SERENE_GRACE); + await game.startBattle([ + Species.TOGEKISS + ]); + + game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; + game.scene.getEnemyParty()[0].stats[Stat.SPD] = 1; + expect(game.scene.getParty()[0].formIndex).toBe(0); + + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + // Check chance of Air Slash with Serene Grace + const phase = game.scene.getCurrentPhase() as MoveEffectPhase; + const move = phase.move.getMove(); + expect(move.id).toBe(Moves.AIR_SLASH); + + const chance = new Utils.IntegerHolder(move.chance); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon(), null, chance, move, phase.getTarget(), false); + expect(chance.value).toBe(60); + + }, 20000); + + //TODO King's Rock Interaction Unit Test +}); diff --git a/src/test/abilities/sheer_force.test.ts b/src/test/abilities/sheer_force.test.ts new file mode 100644 index 00000000000..c01ec176f4d --- /dev/null +++ b/src/test/abilities/sheer_force.test.ts @@ -0,0 +1,208 @@ +import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import {Abilities} from "#enums/abilities"; +import {applyAbAttrs ,applyPreAttackAbAttrs,applyPostDefendAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr, PostDefendTypeChangeAbAttr} from "#app/data/ability"; +import {Species} from "#enums/species"; +import { + CommandPhase, + MoveEffectPhase, +} from "#app/phases"; +import {Mode} from "#app/ui/ui"; +import {Stat} from "#app/data/pokemon-stat"; +import {Moves} from "#enums/moves"; +import {getMovePosition} from "#app/test/utils/gameManagerUtils"; +import {Command} from "#app/ui/command-ui-handler"; +import * as Utils from "#app/utils"; + + +describe("Abilities - Sheer Force", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + const movesToUse = [Moves.AIR_SLASH, Moves.BIND, Moves.CRUSH_CLAW, Moves.TACKLE]; + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.ONIX); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue(movesToUse); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]); + }); + + it("Sheer Force", async() => { + const moveToUse = Moves.AIR_SLASH; + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SHEER_FORCE); + await game.startBattle([ + Species.PIDGEOT + ]); + + + game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; + game.scene.getEnemyParty()[0].stats[Stat.SPD] = 1; + expect(game.scene.getParty()[0].formIndex).toBe(0); + + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + const phase = game.scene.getCurrentPhase() as MoveEffectPhase; + const move = phase.move.getMove(); + expect(move.id).toBe(Moves.AIR_SLASH); + + //Verify the move is boosted and has no chance of secondary effects + const power = new Utils.IntegerHolder(move.power); + const chance = new Utils.IntegerHolder(move.chance); + + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon(), null, chance, move, phase.getTarget(), false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon(), phase.getTarget(), move, power); + + expect(chance.value).toBe(0); + expect(power.value).toBe(move.power * 5461/4096); + + + }, 20000); + + it("Sheer Force with exceptions including binding moves", async() => { + const moveToUse = Moves.BIND; + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SHEER_FORCE); + await game.startBattle([ + Species.PIDGEOT + ]); + + + game.scene.getEnemyParty()[0].stats[Stat.DEF] = 10000; + game.scene.getEnemyParty()[0].stats[Stat.SPD] = 1; + expect(game.scene.getParty()[0].formIndex).toBe(0); + + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + const phase = game.scene.getCurrentPhase() as MoveEffectPhase; + const move = phase.move.getMove(); + expect(move.id).toBe(Moves.BIND); + + //Binding moves and other exceptions are not affected by Sheer Force and have a chance.value of -1 + const power = new Utils.IntegerHolder(move.power); + const chance = new Utils.IntegerHolder(move.chance); + + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon(), null, chance, move, phase.getTarget(), false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon(), phase.getTarget(), move, power); + + expect(chance.value).toBe(-1); + expect(power.value).toBe(move.power); + + + }, 20000); + + it("Sheer Force with moves with no secondary effect", async() => { + const moveToUse = Moves.TACKLE; + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SHEER_FORCE); + await game.startBattle([ + Species.PIDGEOT + ]); + + + game.scene.getEnemyParty()[0].stats[Stat.DEF] = 10000; + game.scene.getEnemyParty()[0].stats[Stat.SPD] = 1; + expect(game.scene.getParty()[0].formIndex).toBe(0); + + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + const phase = game.scene.getCurrentPhase() as MoveEffectPhase; + const move = phase.move.getMove(); + expect(move.id).toBe(Moves.TACKLE); + + //Binding moves and other exceptions are not affected by Sheer Force and have a chance.value of -1 + const power = new Utils.IntegerHolder(move.power); + const chance = new Utils.IntegerHolder(move.chance); + + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon(), null, chance, move, phase.getTarget(), false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon(), phase.getTarget(), move, power); + + expect(chance.value).toBe(-1); + expect(power.value).toBe(move.power); + + + }, 20000); + + it("Sheer Force Disabling Specific Abilities", async() => { + const moveToUse = Moves.CRUSH_CLAW; + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.COLOR_CHANGE); + vi.spyOn(overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "KINGS_ROCK", count: 1}]); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SHEER_FORCE); + await game.startBattle([ + Species.PIDGEOT + ]); + + + game.scene.getEnemyParty()[0].stats[Stat.DEF] = 10000; + game.scene.getEnemyParty()[0].stats[Stat.SPD] = 1; + expect(game.scene.getParty()[0].formIndex).toBe(0); + + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + const phase = game.scene.getCurrentPhase() as MoveEffectPhase; + const move = phase.move.getMove(); + expect(move.id).toBe(Moves.CRUSH_CLAW); + + //Disable color change due to being hit by Sheer Force + const power = new Utils.IntegerHolder(move.power); + const chance = new Utils.IntegerHolder(move.chance); + const user = phase.getUserPokemon(); + const target = phase.getTarget(); + const opponentType = target.getTypes()[0]; + + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, chance, move, target, false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, user, target, move, power); + applyPostDefendAbAttrs(PostDefendTypeChangeAbAttr, target, user, move, target.apply(user, move)); + + expect(chance.value).toBe(0); + expect(power.value).toBe(move.power * 5461/4096); + expect(target.getTypes().length).toBe(2); + expect(target.getTypes()[0]).toBe(opponentType); + + }, 20000); + + //TODO King's Rock Interaction Unit Test +}); diff --git a/src/test/abilities/shield_dust.test.ts b/src/test/abilities/shield_dust.test.ts new file mode 100644 index 00000000000..f52a1d6c7d4 --- /dev/null +++ b/src/test/abilities/shield_dust.test.ts @@ -0,0 +1,79 @@ +import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import { Abilities } from "#enums/abilities"; +import {applyAbAttrs ,applyPreDefendAbAttrs,IgnoreMoveEffectsAbAttr,MoveEffectChanceMultiplierAbAttr} from "#app/data/ability"; +import {Species} from "#enums/species"; +import { + CommandPhase, + MoveEffectPhase, +} from "#app/phases"; +import {Mode} from "#app/ui/ui"; +import {Stat} from "#app/data/pokemon-stat"; +import {Moves} from "#enums/moves"; +import {getMovePosition} from "#app/test/utils/gameManagerUtils"; +import {Command} from "#app/ui/command-ui-handler"; +import * as Utils from "#app/utils"; + + +describe("Abilities - Shield Dust", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + const movesToUse = [Moves.AIR_SLASH]; + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.ONIX); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SHIELD_DUST); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue(movesToUse); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]); + }); + + it("Shield Dust", async() => { + const moveToUse = Moves.AIR_SLASH; + await game.startBattle([ + Species.PIDGEOT + ]); + + + game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; + game.scene.getEnemyParty()[0].stats[Stat.SPD] = 1; + expect(game.scene.getParty()[0].formIndex).toBe(0); + + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + // Shield Dust negates secondary effect + const phase = game.scene.getCurrentPhase() as MoveEffectPhase; + const move = phase.move.getMove(); + expect(move.id).toBe(Moves.AIR_SLASH); + + const chance = new Utils.IntegerHolder(move.chance); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon(), null, chance, move, phase.getTarget(), false); + applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getTarget(),phase.getUserPokemon(),null,null, chance); + expect(chance.value).toBe(0); + + }, 20000); + + //TODO King's Rock Interaction Unit Test +});