From 89e80f3deb5f8652e7dcb9bdb05475b72191260a Mon Sep 17 00:00:00 2001 From: "Amani H." <109637146+xsn34kzx@users.noreply.github.com> Date: Mon, 2 Sep 2024 22:12:34 -0400 Subject: [PATCH] [Refactor/Bug/Move] Overhaul Stats and Battle Items, Implement Several Stat Moves (#2699) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create Getters, Setters, and Types * Work on `pokemon.ts` * Adjust Types, Refactor `White Herb` Modifier * Migrate `TempBattleStat` Usage * Refactor `PokemonBaseStatModifier` Slightly * Remove `BattleStat`, Use "Stat Stages" & New Names * Address Phase `integers` * Finalize `BattleStat` Removal * Address Minor Manual NITs * Apply Own Review Suggestions * Fix Syntax Error * Add Docs * Overhaul X Items * Implement Guard and Power Split with Unit Tests * Add Several Unit Tests and Fixes * Implement Speed Swap with Unit Tests * Fix Keys in Summary Menu * Fix Starf Berry Raising EVA and ACC * Fix Contrary & Simple, Verify with Unit Tests * Implement Power & Guard Swap with Unit Tests * Add Move Effect Message to Speed Swap * Add Move Effect Message to Power & Guard Split * Add Localization Entries * Adjust Last X Item Unit Test * Overhaul X Items Unit Tests * Finish Missing Docs * Revamp Crit-Based Unit Tests & Dire Hit * Address Initial NITs * Apply NIT Batch Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * Fix Moody Test * Address Multiple Messages for `ProtectStatAbAttr` * Change `ignoreOverride` to `bypassSummonData` * Adjust Italian Localization Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> * Fix Moody --------- Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> --- docs/enemy-ai.md | 8 +- src/data/ability.ts | 576 +++++++------- src/data/arena-tag.ts | 16 +- src/data/battle-stat.ts | 71 -- src/data/battler-tags.ts | 76 +- src/data/berry.ts | 27 +- src/data/move.ts | 748 ++++++++++-------- src/data/nature.ts | 7 +- src/data/pokemon-evolutions.ts | 2 +- src/data/pokemon-species.ts | 2 +- src/data/pokemon-stat.ts | 29 - src/data/temp-battle-stat.ts | 38 - src/enums/stat.ts | 67 ++ src/field/pokemon.ts | 387 +++++---- src/interfaces/locales.ts | 3 +- src/locales/de/achv.json | 4 +- src/locales/de/modifier-type.json | 32 +- src/locales/de/modifier.json | 2 +- src/locales/de/move-trigger.json | 6 +- src/locales/de/pokemon-info.json | 1 - src/locales/en/achv.json | 6 +- src/locales/en/modifier-type.json | 27 +- src/locales/en/modifier.json | 2 +- src/locales/en/move-trigger.json | 4 + src/locales/en/pokemon-info.json | 7 +- src/locales/es/achv.json | 4 +- src/locales/es/modifier-type.json | 30 +- src/locales/es/move-trigger.json | 6 +- src/locales/fr/achv.json | 2 +- src/locales/fr/modifier-type.json | 27 +- src/locales/fr/modifier.json | 2 +- src/locales/fr/move-trigger.json | 4 + src/locales/it/achv.json | 4 +- src/locales/it/modifier-type.json | 34 +- src/locales/it/modifier.json | 2 +- src/locales/it/move-trigger.json | 4 + src/locales/ja/achv.json | 2 +- src/locales/ja/modifier-type.json | 32 +- src/locales/ja/modifier.json | 2 +- src/locales/ja/move-trigger.json | 6 +- src/locales/ko/achv.json | 2 +- src/locales/ko/modifier-type.json | 32 +- src/locales/ko/move-trigger.json | 6 +- src/locales/pt_BR/achv.json | 4 +- src/locales/pt_BR/modifier-type.json | 30 +- src/locales/pt_BR/modifier.json | 2 +- src/locales/pt_BR/move-trigger.json | 2 +- src/locales/zh_CN/achv.json | 2 +- src/locales/zh_CN/modifier-type.json | 32 +- src/locales/zh_CN/modifier.json | 2 +- src/locales/zh_CN/move-trigger.json | 4 + src/locales/zh_CN/pokemon-info.json | 4 +- src/locales/zh_TW/achv.json | 4 +- src/locales/zh_TW/modifier-type.json | 32 +- src/locales/zh_TW/modifier.json | 2 +- src/locales/zh_TW/move-trigger.json | 4 + src/modifier/modifier-type.ts | 135 ++-- src/modifier/modifier.ts | 241 ++++-- src/phases/faint-phase.ts | 24 +- src/phases/field-phase.ts | 2 +- src/phases/stat-change-phase.ts | 248 ------ src/phases/stat-stage-change-phase.ts | 244 ++++++ src/phases/turn-start-phase.ts | 4 +- src/system/achv.ts | 17 +- src/system/pokemon-data.ts | 3 +- src/test/abilities/beast_boost.test.ts | 97 +++ src/test/abilities/contrary.test.ts | 42 + src/test/abilities/costar.test.ts | 22 +- src/test/abilities/disguise.test.ts | 26 +- src/test/abilities/flower_gift.test.ts | 18 +- src/test/abilities/gulp_missile.test.ts | 10 +- src/test/abilities/hustle.test.ts | 10 +- src/test/abilities/hyper_cutter.test.ts | 6 +- src/test/abilities/imposter.test.ts | 101 +++ src/test/abilities/intimidate.test.ts | 346 +++----- src/test/abilities/intrepid_sword.test.ts | 17 +- src/test/abilities/moody.test.ts | 44 +- src/test/abilities/moxie.test.ts | 51 +- src/test/abilities/mycelium_might.test.ts | 13 +- src/test/abilities/parental_bond.test.ts | 10 +- src/test/abilities/sand_veil.test.ts | 12 +- src/test/abilities/sap_sipper.test.ts | 62 +- src/test/abilities/serene_grace.test.ts | 2 +- src/test/abilities/sheer_force.test.ts | 2 +- src/test/abilities/shield_dust.test.ts | 2 +- src/test/abilities/simple.test.ts | 42 + src/test/abilities/volt_absorb.test.ts | 8 +- src/test/abilities/wind_rider.test.ts | 76 +- src/test/abilities/zen_mode.test.ts | 4 +- src/test/achievements/achievement.test.ts | 2 +- src/test/battle-stat.spec.ts | 145 ---- src/test/battle/battle.test.ts | 4 +- src/test/battlerTags/octolock.test.ts | 18 +- src/test/battlerTags/stockpiling.test.ts | 80 +- src/test/boss-pokemon.test.ts | 45 +- src/test/items/dire_hit.test.ts | 97 +++ src/test/items/eviolite.test.ts | 14 +- src/test/items/leek.test.ts | 141 ++-- src/test/items/light_ball.test.ts | 14 +- src/test/items/metal_powder.test.ts | 14 +- src/test/items/quick_powder.test.ts | 14 +- src/test/items/scope_lens.test.ts | 53 +- .../items/temp_stat_stage_booster.test.ts | 174 ++++ src/test/items/thick_club.test.ts | 14 +- src/test/localization/battle-stat.test.ts | 217 ----- src/test/moves/alluring_voice.test.ts | 2 +- src/test/moves/baton_pass.test.ts | 17 +- src/test/moves/belly_drum.test.ts | 36 +- src/test/moves/burning_jealousy.test.ts | 8 +- src/test/moves/clangorous_soul.test.ts | 93 ++- src/test/moves/crafty_shield.test.ts | 20 +- src/test/moves/double_team.test.ts | 8 +- src/test/moves/dragon_rage.test.ts | 29 +- src/test/moves/fillet_away.test.ts | 54 +- src/test/moves/fissure.test.ts | 12 +- src/test/moves/flower_shield.test.ts | 42 +- src/test/moves/follow_me.test.ts | 4 +- src/test/moves/freezy_frost.test.ts | 103 +-- src/test/moves/fusion_flare_bolt.test.ts | 2 +- src/test/moves/growth.test.ts | 37 +- src/test/moves/guard_split.test.ts | 82 ++ src/test/moves/guard_swap.test.ts | 63 ++ src/test/moves/haze.test.ts | 43 +- src/test/moves/lash_out.test.ts | 2 +- src/test/moves/make_it_rain.test.ts | 30 +- src/test/moves/mat_block.test.ts | 16 +- src/test/moves/octolock.test.ts | 204 +++-- src/test/moves/parting_shot.test.ts | 50 +- src/test/moves/power_split.test.ts | 82 ++ src/test/moves/power_swap.test.ts | 62 ++ src/test/moves/protect.test.ts | 16 +- src/test/moves/quick_guard.test.ts | 14 +- src/test/moves/speed_swap.test.ts | 54 ++ src/test/moves/spit_up.test.ts | 67 +- src/test/moves/spotlight.test.ts | 4 +- src/test/moves/stockpile.test.ts | 36 +- src/test/moves/swallow.test.ts | 28 +- src/test/moves/tackle.test.ts | 2 +- src/test/moves/tail_whip.test.ts | 23 +- src/test/moves/tailwind.test.ts | 18 +- src/test/moves/tera_blast.test.ts | 7 +- src/test/moves/tidy_up.test.ts | 19 +- src/test/moves/transform.test.ts | 101 +++ src/test/moves/wide_guard.test.ts | 14 +- src/test/utils/helpers/overridesHelper.ts | 11 + src/test/utils/phaseInterceptor.ts | 8 +- src/ui/battle-info.ts | 48 +- src/ui/battle-message-ui-handler.ts | 35 +- src/ui/stats-container.ts | 15 +- src/ui/summary-ui-handler.ts | 12 +- 150 files changed, 3868 insertions(+), 3225 deletions(-) delete mode 100644 src/data/battle-stat.ts delete mode 100644 src/data/pokemon-stat.ts delete mode 100644 src/data/temp-battle-stat.ts delete mode 100644 src/phases/stat-change-phase.ts create mode 100644 src/phases/stat-stage-change-phase.ts create mode 100644 src/test/abilities/beast_boost.test.ts create mode 100644 src/test/abilities/contrary.test.ts create mode 100644 src/test/abilities/imposter.test.ts create mode 100644 src/test/abilities/simple.test.ts delete mode 100644 src/test/battle-stat.spec.ts create mode 100644 src/test/items/dire_hit.test.ts create mode 100644 src/test/items/temp_stat_stage_booster.test.ts delete mode 100644 src/test/localization/battle-stat.test.ts create mode 100644 src/test/moves/guard_split.test.ts create mode 100644 src/test/moves/guard_swap.test.ts create mode 100644 src/test/moves/power_split.test.ts create mode 100644 src/test/moves/power_swap.test.ts create mode 100644 src/test/moves/speed_swap.test.ts create mode 100644 src/test/moves/transform.test.ts diff --git a/docs/enemy-ai.md b/docs/enemy-ai.md index f53a8511893..46482f56a90 100644 --- a/docs/enemy-ai.md +++ b/docs/enemy-ai.md @@ -191,15 +191,15 @@ Now that the enemy Pokémon with the best matchup score is on the field (assumin We then need to apply a 2x multiplier for the move's type effectiveness and a 1.5x multiplier since STAB applies. After applying these multipliers, the final score for this move is **75**. -- **Swords Dance**: As a non-attacking move, this move's benefit score is derived entirely from the sum of its attributes' benefit scores. Swords Dance's `StatChangeAttr` has a user benefit score of 0 and a target benefit score that, in this case, simplifies to +- **Swords Dance**: As a non-attacking move, this move's benefit score is derived entirely from the sum of its attributes' benefit scores. Swords Dance's `StatStageChangeAttr` has a user benefit score of 0 and a target benefit score that, in this case, simplifies to $\text{TBS}=4\times \text{levels} + (-2\times \text{sign(levels)})$ where `levels` is the number of stat stages added by the attribute (in this case, +2). The final score for this move is **6** (Note: because this move is self-targeted, we don't flip the sign of TBS when computing the target score). -- **Crush Claw**: This move is a 75-power Normal-type physical attack with a 50 percent chance to lower the target's Defense by one stage. The additional effect is implemented by the same `StatChangeAttr` as Swords Dance, so we can use the same formulas from before to compute the total TBS and base target score. +- **Crush Claw**: This move is a 75-power Normal-type physical attack with a 50 percent chance to lower the target's Defense by one stage. The additional effect is implemented by the same `StatStageChangeAttr` as Swords Dance, so we can use the same formulas from before to compute the total TBS and base target score. - $\text{TBS}=\text{getTargetBenefitScore(StatChangeAttr)}-\text{attackScore}$ + $\text{TBS}=\text{getTargetBenefitScore(StatStageChangeAttr)}-\text{attackScore}$ $\text{TBS}=(-4 + 2)-(-2\times 2 + \lfloor \frac{75}{5} \rfloor)=-2-11=-13$ @@ -221,4 +221,4 @@ When implementing a new move attribute, it's important to override `MoveAttr`'s - A move's **user benefit score (UBS)** incentivizes (or discourages) the move's usage in general. A positive UBS gives the move more incentive to be used, while a negative UBS gives the move less incentive. - A move's **target benefit score (TBS)** incentivizes (or discourages) the move's usage on a specific target. A positive TBS indicates the move is better used on the user or its allies, while a negative TBS indicates the move is better used on enemies. - **The total benefit score (UBS + TBS) of a move should never be 0.** The move selection algorithm assumes the move's benefit score is unimplemented if the total score is 0 and penalizes the move's usage as a result. With status moves especially, it's important to have some form of implementation among the move's attributes to avoid this scenario. -- **Score functions that use formulas should include comments.** If your attribute requires complex logic or formulas to calculate benefit scores, please add comments to explain how the logic works and its intended effect on the enemy's decision making. \ No newline at end of file +- **Score functions that use formulas should include comments.** If your attribute requires complex logic or formulas to calculate benefit scores, please add comments to explain how the logic works and its intended effect on the enemy's decision making. diff --git a/src/data/ability.ts b/src/data/ability.ts index 40312eaa8be..04dd15f9239 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2,7 +2,6 @@ import Pokemon, { HitResult, PlayerPokemon, PokemonMove } from "../field/pokemon import { Type } from "./type"; import { Constructor } from "#app/utils"; import * as Utils from "../utils"; -import { BattleStat, getBattleStatName } from "./battle-stat"; import { getPokemonNameWithAffix } from "../messages"; import { Weather, WeatherType } from "./weather"; import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags"; @@ -10,12 +9,11 @@ import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, g import { Gender } from "./gender"; import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; -import { Stat, getStatName } from "./pokemon-stat"; import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { TerrainType } from "./terrain"; import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms"; import i18next from "i18next"; -import { Localizable } from "#app/interfaces/locales.js"; +import { Localizable } from "#app/interfaces/locales"; import { Command } from "../ui/command-ui-handler"; import { BerryModifierType } from "#app/modifier/modifier-type"; import { getPokeballName } from "./pokeball"; @@ -25,10 +23,11 @@ import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { Stat, type BattleStat, type EffectiveStat, BATTLE_STATS, EFFECTIVE_STATS, getStatKey } from "#app/enums/stat"; import { MovePhase } from "#app/phases/move-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; -import { StatChangePhase } from "#app/phases/stat-change-phase"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import BattleScene from "#app/battle-scene"; export class Ability implements Localizable { @@ -126,7 +125,7 @@ type AbAttrCondition = (pokemon: Pokemon) => boolean; type PokemonAttackCondition = (user: Pokemon | null, target: Pokemon | null, move: Move) => boolean; type PokemonDefendCondition = (target: Pokemon, user: Pokemon, move: Move) => boolean; -type PokemonStatChangeCondition = (target: Pokemon, statsChanged: BattleStat[], levels: integer) => boolean; +type PokemonStatStageChangeCondition = (target: Pokemon, statsChanged: BattleStat[], stages: number) => boolean; export abstract class AbAttr { public showAbility: boolean; @@ -203,38 +202,36 @@ export class PostBattleInitFormChangeAbAttr extends PostBattleInitAbAttr { } } -export class PostBattleInitStatChangeAbAttr extends PostBattleInitAbAttr { +export class PostBattleInitStatStageChangeAbAttr extends PostBattleInitAbAttr { private stats: BattleStat[]; - private levels: integer; + private stages: number; private selfTarget: boolean; - constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean) { + constructor(stats: BattleStat[], stages: number, selfTarget?: boolean) { super(); - this.stats = typeof(stats) === "number" - ? [ stats as BattleStat ] - : stats as BattleStat[]; - this.levels = levels; + this.stats = stats; + this.stages = stages; this.selfTarget = !!selfTarget; } applyPostBattleInit(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { - const statChangePhases: StatChangePhase[] = []; + const statStageChangePhases: StatStageChangePhase[] = []; if (!simulated) { if (this.selfTarget) { - statChangePhases.push(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.levels)); + statStageChangePhases.push(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages)); } else { for (const opponent of pokemon.getOpponents()) { - statChangePhases.push(new StatChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.levels)); + statStageChangePhases.push(new StatStageChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.stages)); } } - for (const statChangePhase of statChangePhases) { - if (!this.selfTarget && !statChangePhase.getPokemon()?.summonData) { - pokemon.scene.pushPhase(statChangePhase); + for (const statStageChangePhase of statStageChangePhases) { + if (!this.selfTarget && !statStageChangePhase.getPokemon()?.summonData) { + pokemon.scene.pushPhase(statStageChangePhase); } else { // TODO: This causes the ability bar to be shown at the wrong time - pokemon.scene.unshiftPhase(statChangePhase); + pokemon.scene.unshiftPhase(statStageChangePhase); } } } @@ -402,15 +399,15 @@ export class TypeImmunityHealAbAttr extends TypeImmunityAbAttr { } } -class TypeImmunityStatChangeAbAttr extends TypeImmunityAbAttr { +class TypeImmunityStatStageChangeAbAttr extends TypeImmunityAbAttr { private stat: BattleStat; - private levels: integer; + private stages: number; - constructor(immuneType: Type, stat: BattleStat, levels: integer, condition?: AbAttrCondition) { + constructor(immuneType: Type, stat: BattleStat, stages: number, condition?: AbAttrCondition) { super(immuneType, condition); this.stat = stat; - this.levels = levels; + this.stages = stages; } applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { @@ -419,7 +416,7 @@ class TypeImmunityStatChangeAbAttr extends TypeImmunityAbAttr { if (ret) { cancelled.value = true; // Suppresses "No Effect" message if (!simulated) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.levels)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.stages)); } } @@ -559,7 +556,7 @@ export class PostDefendGulpMissileAbAttr extends PostDefendAbAttr { } if (battlerTag.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, attacker.getBattlerIndex(), false, [ BattleStat.DEF ], -1)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, attacker.getBattlerIndex(), false, [ Stat.DEF ], -1)); } else { attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon); } @@ -588,8 +585,8 @@ export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr { } } -export class PostStatChangeAbAttr extends AbAttr { - applyPostStatChange(pokemon: Pokemon, simulated: boolean, statsChanged: BattleStat[], levelChanged: integer, selfTarget: boolean, args: any[]): boolean | Promise { +export class PostStatStageChangeAbAttr extends AbAttr { + applyPostStatStageChange(pokemon: Pokemon, simulated: boolean, statsChanged: BattleStat[], stagesChanged: integer, selfTarget: boolean, args: any[]): boolean | Promise { return false; } } @@ -635,20 +632,20 @@ export class WonderSkinAbAttr extends PreDefendAbAttr { } } -export class MoveImmunityStatChangeAbAttr extends MoveImmunityAbAttr { +export class MoveImmunityStatStageChangeAbAttr extends MoveImmunityAbAttr { private stat: BattleStat; - private levels: integer; + private stages: number; - constructor(immuneCondition: PreDefendAbAttrCondition, stat: BattleStat, levels: integer) { + constructor(immuneCondition: PreDefendAbAttrCondition, stat: BattleStat, stages: number) { super(immuneCondition); this.stat = stat; - this.levels = levels; + this.stages = stages; } applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { const ret = super.applyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args); if (ret && !simulated) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.levels)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.stages)); } return ret; @@ -683,19 +680,19 @@ export class ReverseDrainAbAttr extends PostDefendAbAttr { } } -export class PostDefendStatChangeAbAttr extends PostDefendAbAttr { +export class PostDefendStatStageChangeAbAttr extends PostDefendAbAttr { private condition: PokemonDefendCondition; private stat: BattleStat; - private levels: integer; + private stages: number; private selfTarget: boolean; private allOthers: boolean; - constructor(condition: PokemonDefendCondition, stat: BattleStat, levels: integer, selfTarget: boolean = true, allOthers: boolean = false) { + constructor(condition: PokemonDefendCondition, stat: BattleStat, stages: number, selfTarget: boolean = true, allOthers: boolean = false) { super(true); this.condition = condition; this.stat = stat; - this.levels = levels; + this.stages = stages; this.selfTarget = selfTarget; this.allOthers = allOthers; } @@ -709,11 +706,11 @@ export class PostDefendStatChangeAbAttr extends PostDefendAbAttr { if (this.allOthers) { const otherPokemon = pokemon.getAlly() ? pokemon.getOpponents().concat([ pokemon.getAlly() ]) : pokemon.getOpponents(); for (const other of otherPokemon) { - other.scene.unshiftPhase(new StatChangePhase(other.scene, (other).getBattlerIndex(), false, [ this.stat ], this.levels)); + other.scene.unshiftPhase(new StatStageChangePhase(other.scene, (other).getBattlerIndex(), false, [ this.stat ], this.stages)); } return true; } - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, (this.selfTarget ? pokemon : attacker).getBattlerIndex(), this.selfTarget, [ this.stat ], this.levels)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, (this.selfTarget ? pokemon : attacker).getBattlerIndex(), this.selfTarget, [ this.stat ], this.stages)); return true; } @@ -721,20 +718,20 @@ export class PostDefendStatChangeAbAttr extends PostDefendAbAttr { } } -export class PostDefendHpGatedStatChangeAbAttr extends PostDefendAbAttr { +export class PostDefendHpGatedStatStageChangeAbAttr extends PostDefendAbAttr { private condition: PokemonDefendCondition; private hpGate: number; private stats: BattleStat[]; - private levels: integer; + private stages: number; private selfTarget: boolean; - constructor(condition: PokemonDefendCondition, hpGate: number, stats: BattleStat[], levels: integer, selfTarget: boolean = true) { + constructor(condition: PokemonDefendCondition, hpGate: number, stats: BattleStat[], stages: number, selfTarget: boolean = true) { super(true); this.condition = condition; this.hpGate = hpGate; this.stats = stats; - this.levels = levels; + this.stages = stages; this.selfTarget = selfTarget; } @@ -744,8 +741,8 @@ export class PostDefendHpGatedStatChangeAbAttr extends PostDefendAbAttr { const damageReceived = lastAttackReceived?.damage || 0; if (this.condition(pokemon, attacker, move) && (pokemon.hp <= hpGateFlat && (pokemon.hp + damageReceived) > hpGateFlat)) { - if (!simulated) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, (this.selfTarget ? pokemon : attacker).getBattlerIndex(), true, this.stats, this.levels)); + if (!simulated ) { + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, (this.selfTarget ? pokemon : attacker).getBattlerIndex(), true, this.stats, this.stages)); } return true; } @@ -913,20 +910,20 @@ export class PostDefendContactApplyTagChanceAbAttr extends PostDefendAbAttr { } } -export class PostDefendCritStatChangeAbAttr extends PostDefendAbAttr { +export class PostDefendCritStatStageChangeAbAttr extends PostDefendAbAttr { private stat: BattleStat; - private levels: integer; + private stages: number; - constructor(stat: BattleStat, levels: integer) { + constructor(stat: BattleStat, stages: number) { super(); this.stat = stat; - this.levels = levels; + this.stages = stages; } applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { if (!simulated) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.levels)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.stages)); } return true; @@ -1113,23 +1110,23 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr { } } -export class PostStatChangeStatChangeAbAttr extends PostStatChangeAbAttr { - private condition: PokemonStatChangeCondition; +export class PostStatStageChangeStatStageChangeAbAttr extends PostStatStageChangeAbAttr { + private condition: PokemonStatStageChangeCondition; private statsToChange: BattleStat[]; - private levels: integer; + private stages: number; - constructor(condition: PokemonStatChangeCondition, statsToChange: BattleStat[], levels: integer) { + constructor(condition: PokemonStatStageChangeCondition, statsToChange: BattleStat[], stages: number) { super(true); this.condition = condition; this.statsToChange = statsToChange; - this.levels = levels; + this.stages = stages; } - applyPostStatChange(pokemon: Pokemon, simulated: boolean, statsChanged: BattleStat[], levelsChanged: integer, selfTarget: boolean, args: any[]): boolean { - if (this.condition(pokemon, statsChanged, levelsChanged) && !selfTarget) { + applyPostStatStageChange(pokemon: Pokemon, simulated: boolean, statStagesChanged: BattleStat[], stagesChanged: number, selfTarget: boolean, args: any[]): boolean { + if (this.condition(pokemon, statStagesChanged, stagesChanged) && !selfTarget) { if (!simulated) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, (pokemon).getBattlerIndex(), true, this.statsToChange, this.levels)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, (pokemon).getBattlerIndex(), true, this.statsToChange, this.stages)); } return true; } @@ -1210,13 +1207,13 @@ export class FieldPreventExplosiveMovesAbAttr extends AbAttr { } /** - * Multiplies a BattleStat if the checked Pokemon lacks this ability. + * Multiplies a Stat if the checked Pokemon lacks this ability. * If this ability cannot stack, a BooleanHolder can be used to prevent this from stacking. - * @see {@link applyFieldBattleStatMultiplierAbAttrs} - * @see {@link applyFieldBattleStat} + * @see {@link applyFieldStatMultiplierAbAttrs} + * @see {@link applyFieldStat} * @see {@link Utils.BooleanHolder} */ -export class FieldMultiplyBattleStatAbAttr extends AbAttr { +export class FieldMultiplyStatAbAttr extends AbAttr { private stat: Stat; private multiplier: number; private canStack: boolean; @@ -1230,7 +1227,7 @@ export class FieldMultiplyBattleStatAbAttr extends AbAttr { } /** - * applyFieldBattleStat: Tries to multiply a Pokemon's BattleStat + * applyFieldStat: Tries to multiply a Pokemon's Stat * @param pokemon {@linkcode Pokemon} the Pokemon using this ability * @param passive {@linkcode boolean} unused * @param stat {@linkcode Stat} the type of the checked stat @@ -1240,12 +1237,12 @@ export class FieldMultiplyBattleStatAbAttr extends AbAttr { * @param args {any[]} unused * @returns true if this changed the checked stat, false otherwise. */ - applyFieldBattleStat(pokemon: Pokemon, passive: boolean, simulated: boolean, stat: Stat, statValue: Utils.NumberHolder, checkedPokemon: Pokemon, hasApplied: Utils.BooleanHolder, args: any[]): boolean { + applyFieldStat(pokemon: Pokemon, passive: boolean, simulated: boolean, stat: Stat, statValue: Utils.NumberHolder, checkedPokemon: Pokemon, hasApplied: Utils.BooleanHolder, args: any[]): boolean { if (!this.canStack && hasApplied.value) { return false; } - if (this.stat === stat && checkedPokemon.getAbilityAttrs(FieldMultiplyBattleStatAbAttr).every(attr => (attr as FieldMultiplyBattleStatAbAttr).stat !== stat)) { + if (this.stat === stat && checkedPokemon.getAbilityAttrs(FieldMultiplyStatAbAttr).every(attr => (attr as FieldMultiplyStatAbAttr).stat !== stat)) { statValue.value *= this.multiplier; hasApplied.value = true; return true; @@ -1579,22 +1576,22 @@ export class AllyMoveCategoryPowerBoostAbAttr extends FieldMovePowerBoostAbAttr } } -export class BattleStatMultiplierAbAttr extends AbAttr { - private battleStat: BattleStat; +export class StatMultiplierAbAttr extends AbAttr { + private stat: BattleStat; private multiplier: number; private condition: PokemonAttackCondition | null; - constructor(battleStat: BattleStat, multiplier: number, condition?: PokemonAttackCondition) { + constructor(stat: BattleStat, multiplier: number, condition?: PokemonAttackCondition) { super(false); - this.battleStat = battleStat; + this.stat = stat; this.multiplier = multiplier; this.condition = condition ?? null; } - applyBattleStat(pokemon: Pokemon, passive: boolean, simulated: boolean, battleStat: BattleStat, statValue: Utils.NumberHolder, args: any[]): boolean | Promise { + applyStatStage(pokemon: Pokemon, _passive: boolean, simulated: boolean, stat: BattleStat, statValue: Utils.NumberHolder, args: any[]): boolean | Promise { const move = (args[0] as Move); - if (battleStat === this.battleStat && (!this.condition || this.condition(pokemon, null, move))) { + if (stat === this.stat && (!this.condition || this.condition(pokemon, null, move))) { statValue.value *= this.multiplier; return true; } @@ -1765,15 +1762,15 @@ export class PostVictoryAbAttr extends AbAttr { } } -class PostVictoryStatChangeAbAttr extends PostVictoryAbAttr { +class PostVictoryStatStageChangeAbAttr extends PostVictoryAbAttr { private stat: BattleStat | ((p: Pokemon) => BattleStat); - private levels: integer; + private stages: number; - constructor(stat: BattleStat | ((p: Pokemon) => BattleStat), levels: integer) { + constructor(stat: BattleStat | ((p: Pokemon) => BattleStat), stages: number) { super(); this.stat = stat; - this.levels = levels; + this.stages = stages; } applyPostVictory(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise { @@ -1781,7 +1778,7 @@ class PostVictoryStatChangeAbAttr extends PostVictoryAbAttr { ? this.stat(pokemon) : this.stat; if (!simulated) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], this.levels)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], this.stages)); } return true; } @@ -1815,15 +1812,15 @@ export class PostKnockOutAbAttr extends AbAttr { } } -export class PostKnockOutStatChangeAbAttr extends PostKnockOutAbAttr { +export class PostKnockOutStatStageChangeAbAttr extends PostKnockOutAbAttr { private stat: BattleStat | ((p: Pokemon) => BattleStat); - private levels: integer; + private stages: number; - constructor(stat: BattleStat | ((p: Pokemon) => BattleStat), levels: integer) { + constructor(stat: BattleStat | ((p: Pokemon) => BattleStat), stages: number) { super(); this.stat = stat; - this.levels = levels; + this.stages = stages; } applyPostKnockOut(pokemon: Pokemon, passive: boolean, simulated: boolean, knockedOut: Pokemon, args: any[]): boolean | Promise { @@ -1831,7 +1828,7 @@ export class PostKnockOutStatChangeAbAttr extends PostKnockOutAbAttr { ? this.stat(pokemon) : this.stat; if (!simulated) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], this.levels)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], this.stages)); } return true; } @@ -1855,37 +1852,21 @@ export class CopyFaintedAllyAbilityAbAttr extends PostKnockOutAbAttr { } } -export class IgnoreOpponentStatChangesAbAttr extends AbAttr { - constructor() { +export class IgnoreOpponentStatStagesAbAttr extends AbAttr { + private stats: readonly BattleStat[]; + + constructor(stats?: BattleStat[]) { super(false); + + this.stats = stats ?? BATTLE_STATS; } - apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]) { - (args[0] as Utils.IntegerHolder).value = 0; - - return true; - } -} -/** - * Ignores opponent's evasion stat changes when determining if a move hits or not - * @extends AbAttr - * @see {@linkcode apply} - */ -export class IgnoreOpponentEvasionAbAttr extends AbAttr { - constructor() { - super(false); - } - /** - * Checks if enemy Pokemon is trapped by an Arena Trap-esque ability - * @param pokemon N/A - * @param passive N/A - * @param cancelled N/A - * @param args [0] {@linkcode Utils.IntegerHolder} of BattleStat.EVA - * @returns if evasion level was successfully considered as 0 - */ - apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]) { - (args[0] as Utils.IntegerHolder).value = 0; - return true; + apply(_pokemon: Pokemon, _passive: boolean, simulated: boolean, _cancelled: Utils.BooleanHolder, args: any[]) { + if (this.stats.includes(args[0])) { + (args[1] as Utils.BooleanHolder).value = true; + return true; + } + return false; } } @@ -1907,21 +1888,21 @@ export class IntimidateImmunityAbAttr extends AbAttr { } } -export class PostIntimidateStatChangeAbAttr extends AbAttr { +export class PostIntimidateStatStageChangeAbAttr extends AbAttr { private stats: BattleStat[]; - private levels: integer; + private stages: number; private overwrites: boolean; - constructor(stats: BattleStat[], levels: integer, overwrites?: boolean) { + constructor(stats: BattleStat[], stages: number, overwrites?: boolean) { super(true); this.stats = stats; - this.levels = levels; + this.stages = stages; this.overwrites = !!overwrites; } - apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { + apply(pokemon: Pokemon, passive: boolean, simulated:boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { if (!simulated) { - pokemon.scene.pushPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, this.stats, this.levels)); + pokemon.scene.pushPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, this.stats, this.stages)); } cancelled.value = this.overwrites; return true; @@ -2026,19 +2007,17 @@ export class PostSummonAddBattlerTagAbAttr extends PostSummonAbAttr { } } -export class PostSummonStatChangeAbAttr extends PostSummonAbAttr { +export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr { private stats: BattleStat[]; - private levels: integer; + private stages: number; private selfTarget: boolean; private intimidate: boolean; - constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean, intimidate?: boolean) { + constructor(stats: BattleStat[], stages: number, selfTarget?: boolean, intimidate?: boolean) { super(false); - this.stats = typeof(stats) === "number" - ? [ stats as BattleStat ] - : stats as BattleStat[]; - this.levels = levels; + this.stats = stats; + this.stages = stages; this.selfTarget = !!selfTarget; this.intimidate = !!intimidate; } @@ -2050,20 +2029,19 @@ export class PostSummonStatChangeAbAttr extends PostSummonAbAttr { queueShowAbility(pokemon, passive); // TODO: Better solution than manually showing the ability here if (this.selfTarget) { - // we unshift the StatChangePhase to put it right after the showAbility and not at the end of the + // we unshift the StatStageChangePhase to put it right after the showAbility and not at the end of the // phase list (which could be after CommandPhase for example) - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.levels)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages)); return true; } for (const opponent of pokemon.getOpponents()) { const cancelled = new Utils.BooleanHolder(false); if (this.intimidate) { applyAbAttrs(IntimidateImmunityAbAttr, opponent, cancelled, simulated); - applyAbAttrs(PostIntimidateStatChangeAbAttr, opponent, cancelled, simulated); + applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled, simulated); } if (!cancelled.value) { - const statChangePhase = new StatChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.levels); - pokemon.scene.unshiftPhase(statChangePhase); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.stages)); } } return true; @@ -2104,7 +2082,7 @@ export class PostSummonAllyHealAbAttr extends PostSummonAbAttr { * @param args N/A * @returns if the move was successful */ -export class PostSummonClearAllyStatsAbAttr extends PostSummonAbAttr { +export class PostSummonClearAllyStatStagesAbAttr extends PostSummonAbAttr { constructor() { super(); } @@ -2113,8 +2091,8 @@ export class PostSummonClearAllyStatsAbAttr extends PostSummonAbAttr { const target = pokemon.getAlly(); if (target?.isActive(true)) { if (!simulated) { - for (let s = 0; s < target.summonData.battleStats.length; s++) { - target.summonData.battleStats[s] = 0; + for (const s of BATTLE_STATS) { + target.setStatStage(s, 0); } target.scene.queueMessage(i18next.t("abilityTriggers:postSummonClearAllyStats", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); @@ -2143,7 +2121,7 @@ export class DownloadAbAttr extends PostSummonAbAttr { // TODO: Implement the Substitute feature(s) once move is implemented. /** * Checks to see if it is the opening turn (starting a new game), if so, Download won't work. This is because Download takes into account - * vitamins and items, so it needs to use the BattleStat and the stat alone. + * vitamins and items, so it needs to use the Stat and the stat alone. * @param {Pokemon} pokemon Pokemon that is using the move, as well as seeing the opposing pokemon. * @param {boolean} passive N/A * @param {any[]} args N/A @@ -2156,21 +2134,21 @@ export class DownloadAbAttr extends PostSummonAbAttr { for (const opponent of pokemon.getOpponents()) { this.enemyCountTally++; - this.enemyDef += opponent.getBattleStat(Stat.DEF); - this.enemySpDef += opponent.getBattleStat(Stat.SPDEF); + this.enemyDef += opponent.getEffectiveStat(Stat.DEF); + this.enemySpDef += opponent.getEffectiveStat(Stat.SPDEF); } this.enemyDef = Math.round(this.enemyDef / this.enemyCountTally); this.enemySpDef = Math.round(this.enemySpDef / this.enemyCountTally); if (this.enemyDef < this.enemySpDef) { - this.stats = [BattleStat.ATK]; + this.stats = [ Stat.ATK ]; } else { - this.stats = [BattleStat.SPATK]; + this.stats = [ Stat.SPATK ]; } if (this.enemyDef > 0 && this.enemySpDef > 0) { // only activate if there's actually an enemy to download from if (!simulated) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, this.stats, 1)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, this.stats, 1)); } return true; } @@ -2339,12 +2317,14 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr { } const ally = pokemon.getAlly(); - if (!ally || ally.summonData.battleStats.every((change) => change === 0)) { + if (!ally || ally.getStatStages().every(s => s === 0)) { return false; } if (!simulated) { - pokemon.summonData.battleStats = ally.summonData.battleStats; + for (const s of BATTLE_STATS) { + pokemon.setStatStage(s, ally.getStatStage(s)); + } pokemon.updateInfo(); } @@ -2383,14 +2363,27 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { pokemon.summonData.ability = target.getAbility().id; pokemon.summonData.gender = target.getGender(); pokemon.summonData.fusionGender = target.getFusionGender(); - pokemon.summonData.stats = [ pokemon.stats[Stat.HP] ].concat(target.stats.slice(1)); - pokemon.summonData.battleStats = target.summonData.battleStats.slice(0); + + // Copy all stats (except HP) + for (const s of EFFECTIVE_STATS) { + pokemon.setStat(s, target.getStat(s, false), false); + } + + // Copy all stat stages + for (const s of BATTLE_STATS) { + pokemon.setStatStage(s, target.getStatStage(s)); + } + pokemon.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m!.moveId, m!.ppUsed, m!.ppUp)); // TODO: are those bangs correct? pokemon.summonData.types = target.getTypes(); + pokemon.scene.playSound("battle_anims/PRSFX- Transform"); - pokemon.loadAssets(false).then(() => pokemon.playAnim()); + pokemon.loadAssets(false).then(() => { + pokemon.playAnim(); + pokemon.updateInfo(); + }); pokemon.scene.queueMessage(i18next.t("abilityTriggers:postSummonTransform", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), targetName: target.name, })); @@ -2594,13 +2587,13 @@ export class PreSwitchOutFormChangeAbAttr extends PreSwitchOutAbAttr { } -export class PreStatChangeAbAttr extends AbAttr { - applyPreStatChange(pokemon: Pokemon | null, passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise { +export class PreStatStageChangeAbAttr extends AbAttr { + applyPreStatStageChange(pokemon: Pokemon | null, passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise { return false; } } -export class ProtectStatAbAttr extends PreStatChangeAbAttr { +export class ProtectStatAbAttr extends PreStatStageChangeAbAttr { private protectedStat?: BattleStat; constructor(protectedStat?: BattleStat) { @@ -2609,7 +2602,7 @@ export class ProtectStatAbAttr extends PreStatChangeAbAttr { this.protectedStat = protectedStat; } - applyPreStatChange(pokemon: Pokemon, passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean { + applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, _args: any[]): boolean { if (Utils.isNullOrUndefined(this.protectedStat) || stat === this.protectedStat) { cancelled.value = true; return true; @@ -2618,11 +2611,11 @@ export class ProtectStatAbAttr extends PreStatChangeAbAttr { return false; } - getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { + getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string { return i18next.t("abilityTriggers:protectStat", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName, - statName: this.protectedStat ? getBattleStatName(this.protectedStat) : i18next.t("battle:stats") + statName: this.protectedStat ? i18next.t(getStatKey(this.protectedStat)) : i18next.t("battle:stats") }); } } @@ -3465,51 +3458,53 @@ export class MoodyAbAttr extends PostTurnAbAttr { super(true); } /** - * Randomly increases one BattleStat by 2 stages and decreases a different BattleStat by 1 stage + * Randomly increases one stat stage by 2 and decreases a different stat stage by 1 * @param {Pokemon} pokemon Pokemon that has this ability * @param passive N/A * @param simulated true if applying in a simulated call. * @param args N/A * @returns true * - * Any BattleStats at +6 or -6 are excluded from being increased or decreased, respectively - * If the pokemon already has all BattleStats raised to stage 6, it will only decrease one BattleStat by 1 stage - * If the pokemon already has all BattleStats lowered to stage -6, it will only increase one BattleStat by 2 stages + * Any stat stages at +6 or -6 are excluded from being increased or decreased, respectively + * If the pokemon already has all stat stages raised to 6, it will only decrease one stat stage by 1 + * If the pokemon already has all stat stages lowered to -6, it will only increase one stat stage by 2 */ applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { - const selectableStats = [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD]; - const increaseStatArray = selectableStats.filter(s => pokemon.summonData.battleStats[s] < 6); - let decreaseStatArray = selectableStats.filter(s => pokemon.summonData.battleStats[s] > -6); + const canRaise = EFFECTIVE_STATS.filter(s => pokemon.getStatStage(s) < 6); + let canLower = EFFECTIVE_STATS.filter(s => pokemon.getStatStage(s) > -6); - if (!simulated && increaseStatArray.length > 0) { - const increaseStat = increaseStatArray[Utils.randInt(increaseStatArray.length)]; - decreaseStatArray = decreaseStatArray.filter(s => s !== increaseStat); - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [increaseStat], 2)); - } - if (!simulated && decreaseStatArray.length > 0) { - const decreaseStat = decreaseStatArray[Utils.randInt(decreaseStatArray.length)]; - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [decreaseStat], -1)); + if (!simulated) { + if (canRaise.length > 0) { + const raisedStat = Utils.randSeedItem(canRaise); + canLower = canRaise.filter(s => s !== raisedStat); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ raisedStat ], 2)); + } + if (canLower.length > 0) { + const loweredStat = Utils.randSeedItem(canLower); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ loweredStat ], -1)); + } } + return true; } } -export class PostTurnStatChangeAbAttr extends PostTurnAbAttr { +export class PostTurnStatStageChangeAbAttr extends PostTurnAbAttr { private stats: BattleStat[]; - private levels: integer; + private stages: number; - constructor(stats: BattleStat | BattleStat[], levels: integer) { + constructor(stats: BattleStat[], stages: number) { super(true); this.stats = Array.isArray(stats) ? stats : [ stats ]; - this.levels = levels; + this.stages = stages; } applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { if (!simulated) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.levels)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages)); } return true; } @@ -3721,7 +3716,7 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { } } -export class StatChangeMultiplierAbAttr extends AbAttr { +export class StatStageChangeMultiplierAbAttr extends AbAttr { private multiplier: integer; constructor(multiplier: integer) { @@ -3737,10 +3732,10 @@ export class StatChangeMultiplierAbAttr extends AbAttr { } } -export class StatChangeCopyAbAttr extends AbAttr { +export class StatStageChangeCopyAbAttr extends AbAttr { apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise { if (!simulated) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, (args[0] as BattleStat[]), (args[1] as integer), true, false, false)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, (args[0] as BattleStat[]), (args[1] as number), true, false, false)); } return true; } @@ -4140,22 +4135,22 @@ export class FlinchEffectAbAttr extends AbAttr { } } -export class FlinchStatChangeAbAttr extends FlinchEffectAbAttr { +export class FlinchStatStageChangeAbAttr extends FlinchEffectAbAttr { private stats: BattleStat[]; - private levels: integer; + private stages: number; - constructor(stats: BattleStat | BattleStat[], levels: integer) { + constructor(stats: BattleStat[], stages: number) { super(); this.stats = Array.isArray(stats) ? stats : [ stats ]; - this.levels = levels; + this.stages = stages; } apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { if (!simulated) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.levels)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages)); } return true; } @@ -4355,9 +4350,9 @@ export class MoneyAbAttr extends PostBattleAbAttr { * Applies a stat change after a Pokémon is summoned, * conditioned on the presence of a specific arena tag. * - * @extends {PostSummonStatChangeAbAttr} + * @extends {PostSummonStatStageChangeAbAttr} */ -export class PostSummonStatChangeOnArenaAbAttr extends PostSummonStatChangeAbAttr { +export class PostSummonStatStageChangeOnArenaAbAttr extends PostSummonStatStageChangeAbAttr { /** * The type of arena tag that conditions the stat change. * @private @@ -4366,13 +4361,13 @@ export class PostSummonStatChangeOnArenaAbAttr extends PostSummonStatChangeAbAtt private tagType: ArenaTagType; /** - * Creates an instance of PostSummonStatChangeOnArenaAbAttr. + * Creates an instance of PostSummonStatStageChangeOnArenaAbAttr. * Initializes the stat change to increase Attack by 1 stage if the specified arena tag is present. * * @param {ArenaTagType} tagType - The type of arena tag to check for. */ constructor(tagType: ArenaTagType) { - super([BattleStat.ATK], 1, true, false); + super([ Stat.ATK ], 1, true, false); this.tagType = tagType; } @@ -4619,14 +4614,14 @@ export function applyPostMoveUsedAbAttrs(attrType: Constructor(attrType, pokemon, (attr, passive) => attr.applyPostMoveUsed(pokemon, move, source, targets, simulated, args), args, false, simulated); } -export function applyBattleStatMultiplierAbAttrs(attrType: Constructor, - pokemon: Pokemon, battleStat: BattleStat, statValue: Utils.NumberHolder, simulated: boolean = false, ...args: any[]): Promise { - return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyBattleStat(pokemon, passive, simulated, battleStat, statValue, args), args, false, simulated); +export function applyStatMultiplierAbAttrs(attrType: Constructor, + pokemon: Pokemon, stat: BattleStat, statValue: Utils.NumberHolder, simulated: boolean = false, ...args: any[]): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyStatStage(pokemon, passive, simulated, stat, statValue, args), args); } /** - * Applies a field Battle Stat multiplier attribute - * @param attrType {@linkcode FieldMultiplyBattleStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being + * Applies a field Stat multiplier attribute + * @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being * @param pokemon {@linkcode Pokemon} the Pokemon applying this ability * @param stat {@linkcode Stat} the type of the checked stat * @param statValue {@linkcode Utils.NumberHolder} the value of the checked stat @@ -4634,9 +4629,9 @@ export function applyBattleStatMultiplierAbAttrs(attrType: Constructor, +export function applyFieldStatMultiplierAbAttrs(attrType: Constructor, pokemon: Pokemon, stat: Stat, statValue: Utils.NumberHolder, checkedPokemon: Pokemon, hasApplied: Utils.BooleanHolder, simulated: boolean = false, ...args: any[]): Promise { - return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyFieldBattleStat(pokemon, passive, simulated, stat, statValue, checkedPokemon, hasApplied, args), args, false, simulated); + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyFieldStat(pokemon, passive, simulated, stat, statValue, checkedPokemon, hasApplied, args), args); } export function applyPreAttackAbAttrs(attrType: Constructor, @@ -4669,14 +4664,14 @@ export function applyPreSwitchOutAbAttrs(attrType: Constructor(attrType, pokemon, (attr, passive) => attr.applyPreSwitchOut(pokemon, passive, simulated, args), args, true, simulated); } -export function applyPreStatChangeAbAttrs(attrType: Constructor, +export function applyPreStatStageChangeAbAttrs(attrType: Constructor, pokemon: Pokemon | null, stat: BattleStat, cancelled: Utils.BooleanHolder, simulated: boolean = false, ...args: any[]): Promise { - return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPreStatChange(pokemon, passive, simulated, stat, cancelled, args), args, false, simulated); + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPreStatStageChange(pokemon, passive, simulated, stat, cancelled, args), args, false, simulated); } -export function applyPostStatChangeAbAttrs(attrType: Constructor, - pokemon: Pokemon, stats: BattleStat[], levels: integer, selfTarget: boolean, simulated: boolean = false, ...args: any[]): Promise { - return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostStatChange(pokemon, simulated, stats, levels, selfTarget, args), args, false, simulated); +export function applyPostStatStageChangeAbAttrs(attrType: Constructor, + pokemon: Pokemon, stats: BattleStat[], stages: integer, selfTarget: boolean, simulated: boolean = false, ...args: any[]): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, _passive) => attr.applyPostStatStageChange(pokemon, simulated, stats, stages, selfTarget, args), args, false, simulated); } export function applyPreSetStatusAbAttrs(attrType: Constructor, @@ -4766,7 +4761,7 @@ export function initAbilities() { .attr(PostSummonWeatherChangeAbAttr, WeatherType.RAIN) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.RAIN), new Ability(Abilities.SPEED_BOOST, 3) - .attr(PostTurnStatChangeAbAttr, BattleStat.SPD, 1), + .attr(PostTurnStatStageChangeAbAttr, [ Stat.SPD ], 1), new Ability(Abilities.BATTLE_ARMOR, 3) .attr(BlockCritAbAttr) .ignorable(), @@ -4781,7 +4776,7 @@ export function initAbilities() { .attr(StatusEffectImmunityAbAttr, StatusEffect.PARALYSIS) .ignorable(), new Ability(Abilities.SAND_VEIL, 3) - .attr(BattleStatMultiplierAbAttr, BattleStat.EVA, 1.2) + .attr(StatMultiplierAbAttr, Stat.EVA, 1.2) .attr(BlockWeatherDamageAttr, WeatherType.SANDSTORM) .condition(getWeatherCondition(WeatherType.SANDSTORM)) .ignorable(), @@ -4807,7 +4802,7 @@ export function initAbilities() { .attr(PostFaintUnsuppressedWeatherFormChangeAbAttr) .bypassFaint(), new Ability(Abilities.COMPOUND_EYES, 3) - .attr(BattleStatMultiplierAbAttr, BattleStat.ACC, 1.3), + .attr(StatMultiplierAbAttr, Stat.ACC, 1.3), new Ability(Abilities.INSOMNIA, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP) .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY) @@ -4832,7 +4827,7 @@ export function initAbilities() { .attr(ForceSwitchOutImmunityAbAttr) .ignorable(), new Ability(Abilities.INTIMIDATE, 3) - .attr(PostSummonStatChangeAbAttr, BattleStat.ATK, -1, false, true), + .attr(PostSummonStatStageChangeAbAttr, [ Stat.ATK ], -1, false, true), new Ability(Abilities.SHADOW_TAG, 3) .attr(ArenaTrapAbAttr, (user, target) => { if (target.hasAbility(Abilities.SHADOW_TAG)) { @@ -4863,26 +4858,26 @@ export function initAbilities() { .attr(PreSwitchOutResetStatusAbAttr), new Ability(Abilities.LIGHTNING_ROD, 3) .attr(RedirectTypeMoveAbAttr, Type.ELECTRIC) - .attr(TypeImmunityStatChangeAbAttr, Type.ELECTRIC, BattleStat.SPATK, 1) + .attr(TypeImmunityStatStageChangeAbAttr, Type.ELECTRIC, Stat.SPATK, 1) .ignorable(), new Ability(Abilities.SERENE_GRACE, 3) .attr(MoveEffectChanceMultiplierAbAttr, 2) .partial(), new Ability(Abilities.SWIFT_SWIM, 3) - .attr(BattleStatMultiplierAbAttr, BattleStat.SPD, 2) + .attr(StatMultiplierAbAttr, Stat.SPD, 2) .condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)), new Ability(Abilities.CHLOROPHYLL, 3) - .attr(BattleStatMultiplierAbAttr, BattleStat.SPD, 2) + .attr(StatMultiplierAbAttr, Stat.SPD, 2) .condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)), new Ability(Abilities.ILLUMINATE, 3) - .attr(ProtectStatAbAttr, BattleStat.ACC) + .attr(ProtectStatAbAttr, Stat.ACC) .attr(DoubleBattleChanceAbAttr) .ignorable(), new Ability(Abilities.TRACE, 3) .attr(PostSummonCopyAbilityAbAttr) .attr(UncopiableAbilityAbAttr), new Ability(Abilities.HUGE_POWER, 3) - .attr(BattleStatMultiplierAbAttr, BattleStat.ATK, 2), + .attr(StatMultiplierAbAttr, Stat.ATK, 2), new Ability(Abilities.POISON_POINT, 3) .attr(PostDefendContactApplyStatusEffectAbAttr, 30, StatusEffect.POISON) .bypassFaint(), @@ -4927,25 +4922,25 @@ export function initAbilities() { new Ability(Abilities.RUN_AWAY, 3) .attr(RunSuccessAbAttr), new Ability(Abilities.KEEN_EYE, 3) - .attr(ProtectStatAbAttr, BattleStat.ACC) + .attr(ProtectStatAbAttr, Stat.ACC) .ignorable(), new Ability(Abilities.HYPER_CUTTER, 3) - .attr(ProtectStatAbAttr, BattleStat.ATK) + .attr(ProtectStatAbAttr, Stat.ATK) .ignorable(), new Ability(Abilities.PICKUP, 3) .attr(PostBattleLootAbAttr), new Ability(Abilities.TRUANT, 3) .attr(PostSummonAddBattlerTagAbAttr, BattlerTagType.TRUANT, 1, false), new Ability(Abilities.HUSTLE, 3) - .attr(BattleStatMultiplierAbAttr, BattleStat.ATK, 1.5) - .attr(BattleStatMultiplierAbAttr, BattleStat.ACC, 0.8, (user, target, move) => move.category === MoveCategory.PHYSICAL), + .attr(StatMultiplierAbAttr, Stat.ATK, 1.5) + .attr(StatMultiplierAbAttr, Stat.ACC, 0.8, (_user, _target, move) => move.category === MoveCategory.PHYSICAL), new Ability(Abilities.CUTE_CHARM, 3) .attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED), new Ability(Abilities.PLUS, 3) - .conditionalAttr(p => p.scene.currentBattle.double && [Abilities.PLUS, Abilities.MINUS].some(a => p.getAlly().hasAbility(a)), BattleStatMultiplierAbAttr, BattleStat.SPATK, 1.5) + .conditionalAttr(p => p.scene.currentBattle.double && [Abilities.PLUS, Abilities.MINUS].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5) .ignorable(), new Ability(Abilities.MINUS, 3) - .conditionalAttr(p => p.scene.currentBattle.double && [Abilities.PLUS, Abilities.MINUS].some(a => p.getAlly().hasAbility(a)), BattleStatMultiplierAbAttr, BattleStat.SPATK, 1.5) + .conditionalAttr(p => p.scene.currentBattle.double && [Abilities.PLUS, Abilities.MINUS].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5) .ignorable(), new Ability(Abilities.FORECAST, 3) .attr(UncopiableAbilityAbAttr) @@ -4960,9 +4955,9 @@ export function initAbilities() { .conditionalAttr(pokemon => !Utils.randSeedInt(3), PostTurnResetStatusAbAttr), new Ability(Abilities.GUTS, 3) .attr(BypassBurnDamageReductionAbAttr) - .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), BattleStatMultiplierAbAttr, BattleStat.ATK, 1.5), + .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), StatMultiplierAbAttr, Stat.ATK, 1.5), new Ability(Abilities.MARVEL_SCALE, 3) - .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), BattleStatMultiplierAbAttr, BattleStat.DEF, 1.5) + .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), StatMultiplierAbAttr, Stat.DEF, 1.5) .ignorable(), new Ability(Abilities.LIQUID_OOZE, 3) .attr(ReverseDrainAbAttr), @@ -4995,7 +4990,7 @@ export function initAbilities() { .attr(ProtectStatAbAttr) .ignorable(), new Ability(Abilities.PURE_POWER, 3) - .attr(BattleStatMultiplierAbAttr, BattleStat.ATK, 2), + .attr(StatMultiplierAbAttr, Stat.ATK, 2), new Ability(Abilities.SHELL_ARMOR, 3) .attr(BlockCritAbAttr) .ignorable(), @@ -5006,25 +5001,25 @@ export function initAbilities() { .attr(PostFaintUnsuppressedWeatherFormChangeAbAttr) .bypassFaint(), new Ability(Abilities.TANGLED_FEET, 4) - .conditionalAttr(pokemon => !!pokemon.getTag(BattlerTagType.CONFUSED), BattleStatMultiplierAbAttr, BattleStat.EVA, 2) + .conditionalAttr(pokemon => !!pokemon.getTag(BattlerTagType.CONFUSED), StatMultiplierAbAttr, Stat.EVA, 2) .ignorable(), new Ability(Abilities.MOTOR_DRIVE, 4) - .attr(TypeImmunityStatChangeAbAttr, Type.ELECTRIC, BattleStat.SPD, 1) + .attr(TypeImmunityStatStageChangeAbAttr, Type.ELECTRIC, Stat.SPD, 1) .ignorable(), new Ability(Abilities.RIVALRY, 4) .attr(MovePowerBoostAbAttr, (user, target, move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender === target?.gender, 1.25, true) .attr(MovePowerBoostAbAttr, (user, target, move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender !== target?.gender, 0.75), new Ability(Abilities.STEADFAST, 4) - .attr(FlinchStatChangeAbAttr, BattleStat.SPD, 1), + .attr(FlinchStatStageChangeAbAttr, [ Stat.SPD ], 1), new Ability(Abilities.SNOW_CLOAK, 4) - .attr(BattleStatMultiplierAbAttr, BattleStat.EVA, 1.2) + .attr(StatMultiplierAbAttr, Stat.EVA, 1.2) .attr(BlockWeatherDamageAttr, WeatherType.HAIL) .condition(getWeatherCondition(WeatherType.HAIL, WeatherType.SNOW)) .ignorable(), new Ability(Abilities.GLUTTONY, 4) .attr(ReduceBerryUseThresholdAbAttr), new Ability(Abilities.ANGER_POINT, 4) - .attr(PostDefendCritStatChangeAbAttr, BattleStat.ATK, 6), + .attr(PostDefendCritStatStageChangeAbAttr, Stat.ATK, 6), new Ability(Abilities.UNBURDEN, 4) .unimplemented(), new Ability(Abilities.HEATPROOF, 4) @@ -5032,7 +5027,7 @@ export function initAbilities() { .attr(ReduceBurnDamageAbAttr, 0.5) .ignorable(), new Ability(Abilities.SIMPLE, 4) - .attr(StatChangeMultiplierAbAttr, 2) + .attr(StatStageChangeMultiplierAbAttr, 2) .ignorable(), new Ability(Abilities.DRY_SKIN, 4) .attr(PostWeatherLapseDamageAbAttr, 2, WeatherType.SUNNY, WeatherType.HARSH_SUN) @@ -5057,11 +5052,11 @@ export function initAbilities() { .condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)), new Ability(Abilities.SOLAR_POWER, 4) .attr(PostWeatherLapseDamageAbAttr, 2, WeatherType.SUNNY, WeatherType.HARSH_SUN) - .attr(BattleStatMultiplierAbAttr, BattleStat.SPATK, 1.5) + .attr(StatMultiplierAbAttr, Stat.SPATK, 1.5) .condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)), new Ability(Abilities.QUICK_FEET, 4) - .conditionalAttr(pokemon => pokemon.status ? pokemon.status.effect === StatusEffect.PARALYSIS : false, BattleStatMultiplierAbAttr, BattleStat.SPD, 2) - .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), BattleStatMultiplierAbAttr, BattleStat.SPD, 1.5), + .conditionalAttr(pokemon => pokemon.status ? pokemon.status.effect === StatusEffect.PARALYSIS : false, StatMultiplierAbAttr, Stat.SPD, 2) + .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), StatMultiplierAbAttr, Stat.SPD, 1.5), new Ability(Abilities.NORMALIZE, 4) .attr(MoveTypeChangeAbAttr, Type.NORMAL, 1.2, (user, target, move) => { return ![Moves.HIDDEN_POWER, Moves.WEATHER_BALL, Moves.NATURAL_GIFT, Moves.JUDGMENT, Moves.TECHNO_BLAST].includes(move.id); @@ -5101,7 +5096,7 @@ export function initAbilities() { new Ability(Abilities.FOREWARN, 4) .attr(ForewarnAbAttr), new Ability(Abilities.UNAWARE, 4) - .attr(IgnoreOpponentStatChangesAbAttr) + .attr(IgnoreOpponentStatStagesAbAttr) .ignorable(), new Ability(Abilities.TINTED_LENS, 4) //@ts-ignore @@ -5116,7 +5111,7 @@ export function initAbilities() { .attr(IntimidateImmunityAbAttr), new Ability(Abilities.STORM_DRAIN, 4) .attr(RedirectTypeMoveAbAttr, Type.WATER) - .attr(TypeImmunityStatChangeAbAttr, Type.WATER, BattleStat.SPATK, 1) + .attr(TypeImmunityStatStageChangeAbAttr, Type.WATER, Stat.SPATK, 1) .ignorable(), new Ability(Abilities.ICE_BODY, 4) .attr(BlockWeatherDamageAttr, WeatherType.HAIL) @@ -5140,8 +5135,8 @@ export function initAbilities() { .attr(UnsuppressableAbilityAbAttr) .attr(NoFusionAbilityAbAttr), new Ability(Abilities.FLOWER_GIFT, 4) - .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), BattleStatMultiplierAbAttr, BattleStat.ATK, 1.5) - .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), BattleStatMultiplierAbAttr, BattleStat.SPDEF, 1.5) + .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.ATK, 1.5) + .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.SPDEF, 1.5) .attr(UncopiableAbilityAbAttr) .attr(NoFusionAbilityAbAttr) .attr(PostSummonFormChangeByWeatherAbAttr, Abilities.FLOWER_GIFT) @@ -5158,15 +5153,15 @@ export function initAbilities() { .attr(MoveEffectChanceMultiplierAbAttr, 0) .partial(), new Ability(Abilities.CONTRARY, 5) - .attr(StatChangeMultiplierAbAttr, -1) + .attr(StatStageChangeMultiplierAbAttr, -1) .ignorable(), new Ability(Abilities.UNNERVE, 5) .attr(PreventBerryUseAbAttr), new Ability(Abilities.DEFIANT, 5) - .attr(PostStatChangeStatChangeAbAttr, (target, statsChanged, levels) => levels < 0, [BattleStat.ATK], 2), + .attr(PostStatStageChangeStatStageChangeAbAttr, (target, statsChanged, stages) => stages < 0, [Stat.ATK], 2), new Ability(Abilities.DEFEATIST, 5) - .attr(BattleStatMultiplierAbAttr, BattleStat.ATK, 0.5) - .attr(BattleStatMultiplierAbAttr, BattleStat.SPATK, 0.5) + .attr(StatMultiplierAbAttr, Stat.ATK, 0.5) + .attr(StatMultiplierAbAttr, Stat.SPATK, 0.5) .condition((pokemon) => pokemon.getHpRatio() <= 0.5), new Ability(Abilities.CURSED_BODY, 5) .attr(PostDefendMoveDisableAbAttr, 30) @@ -5177,8 +5172,8 @@ export function initAbilities() { .ignorable() .unimplemented(), new Ability(Abilities.WEAK_ARMOR, 5) - .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, BattleStat.DEF, -1) - .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, BattleStat.SPD, 2), + .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.DEF, -1) + .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.SPD, 2), new Ability(Abilities.HEAVY_METAL, 5) .attr(WeightMultiplierAbAttr, 2) .ignorable(), @@ -5214,10 +5209,10 @@ export function initAbilities() { new Ability(Abilities.REGENERATOR, 5) .attr(PreSwitchOutHealAbAttr), new Ability(Abilities.BIG_PECKS, 5) - .attr(ProtectStatAbAttr, BattleStat.DEF) + .attr(ProtectStatAbAttr, Stat.DEF) .ignorable(), new Ability(Abilities.SAND_RUSH, 5) - .attr(BattleStatMultiplierAbAttr, BattleStat.SPD, 2) + .attr(StatMultiplierAbAttr, Stat.SPD, 2) .attr(BlockWeatherDamageAttr, WeatherType.SANDSTORM) .condition(getWeatherCondition(WeatherType.SANDSTORM)), new Ability(Abilities.WONDER_SKIN, 5) @@ -5239,18 +5234,18 @@ export function initAbilities() { .attr(PostDefendAbilityGiveAbAttr, Abilities.MUMMY) .bypassFaint(), new Ability(Abilities.MOXIE, 5) - .attr(PostVictoryStatChangeAbAttr, BattleStat.ATK, 1), + .attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1), new Ability(Abilities.JUSTIFIED, 5) - .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.type === Type.DARK && move.category !== MoveCategory.STATUS, BattleStat.ATK, 1), + .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.type === Type.DARK && move.category !== MoveCategory.STATUS, Stat.ATK, 1), new Ability(Abilities.RATTLED, 5) - .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS && (move.type === Type.DARK || move.type === Type.BUG || - move.type === Type.GHOST), BattleStat.SPD, 1) - .attr(PostIntimidateStatChangeAbAttr, [BattleStat.SPD], 1), + .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS && (move.type === Type.DARK || move.type === Type.BUG || + move.type === Type.GHOST), Stat.SPD, 1) + .attr(PostIntimidateStatStageChangeAbAttr, [Stat.SPD], 1), new Ability(Abilities.MAGIC_BOUNCE, 5) .ignorable() .unimplemented(), new Ability(Abilities.SAP_SIPPER, 5) - .attr(TypeImmunityStatChangeAbAttr, Type.GRASS, BattleStat.ATK, 1) + .attr(TypeImmunityStatStageChangeAbAttr, Type.GRASS, Stat.ATK, 1) .ignorable(), new Ability(Abilities.PRANKSTER, 5) .attr(ChangeMovePriorityAbAttr, (pokemon, move: Move) => move.category === MoveCategory.STATUS, 1), @@ -5273,7 +5268,7 @@ export function initAbilities() { .attr(NoFusionAbilityAbAttr) .bypassFaint(), new Ability(Abilities.VICTORY_STAR, 5) - .attr(BattleStatMultiplierAbAttr, BattleStat.ACC, 1.1) + .attr(StatMultiplierAbAttr, Stat.ACC, 1.1) .partial(), new Ability(Abilities.TURBOBLAZE, 5) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTurboblaze", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) @@ -5302,7 +5297,7 @@ export function initAbilities() { .attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.hasFlag(MoveFlags.BALLBOMB_MOVE)) .ignorable(), new Ability(Abilities.COMPETITIVE, 6) - .attr(PostStatChangeStatChangeAbAttr, (target, statsChanged, levels) => levels < 0, [BattleStat.SPATK], 2), + .attr(PostStatStageChangeStatStageChangeAbAttr, (target, statsChanged, stages) => stages < 0, [Stat.SPATK], 2), new Ability(Abilities.STRONG_JAW, 6) .attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.BITING_MOVE), 1.5), new Ability(Abilities.REFRIGERATE, 6) @@ -5322,7 +5317,7 @@ export function initAbilities() { new Ability(Abilities.MEGA_LAUNCHER, 6) .attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.PULSE_MOVE), 1.5), new Ability(Abilities.GRASS_PELT, 6) - .conditionalAttr(getTerrainCondition(TerrainType.GRASSY), BattleStatMultiplierAbAttr, BattleStat.DEF, 1.5) + .conditionalAttr(getTerrainCondition(TerrainType.GRASSY), StatMultiplierAbAttr, Stat.DEF, 1.5) .ignorable(), new Ability(Abilities.SYMBIOSIS, 6) .unimplemented(), @@ -5331,7 +5326,7 @@ export function initAbilities() { new Ability(Abilities.PIXILATE, 6) .attr(MoveTypeChangeAbAttr, Type.FAIRY, 1.2, (user, target, move) => move.type === Type.NORMAL && !move.hasAttr(VariableMoveTypeAttr)), new Ability(Abilities.GOOEY, 6) - .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), BattleStat.SPD, -1, false), + .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), Stat.SPD, -1, false), new Ability(Abilities.AERILATE, 6) .attr(MoveTypeChangeAbAttr, Type.FLYING, 1.2, (user, target, move) => move.type === Type.NORMAL && !move.hasAttr(VariableMoveTypeAttr)), new Ability(Abilities.PARENTAL_BOND, 6) @@ -5365,7 +5360,7 @@ export function initAbilities() { .attr(PostFaintClearWeatherAbAttr) .bypassFaint(), new Ability(Abilities.STAMINA, 7) - .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, BattleStat.DEF, 1), + .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1), new Ability(Abilities.WIMP_OUT, 7) .condition(getSheerForceHitDisableAbCondition()) .unimplemented(), @@ -5373,7 +5368,7 @@ export function initAbilities() { .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), + .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.type === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2), new Ability(Abilities.MERCILESS, 7) .attr(ConditionalCritAbAttr, (user, target, move) => target?.status?.effect === StatusEffect.TOXIC || target?.status?.effect === StatusEffect.POISON), new Ability(Abilities.SHIELDS_DOWN, 7) @@ -5397,10 +5392,10 @@ 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(PostDefendHpGatedStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [Stat.SPATK], 1) .condition(getSheerForceHitDisableAbCondition()), new Ability(Abilities.SLUSH_RUSH, 7) - .attr(BattleStatMultiplierAbAttr, BattleStat.SPD, 2) + .attr(StatMultiplierAbAttr, Stat.SPD, 2) .condition(getWeatherCondition(WeatherType.HAIL, WeatherType.SNOW)), new Ability(Abilities.LONG_REACH, 7) .attr(IgnoreContactAbAttr), @@ -5411,7 +5406,7 @@ export function initAbilities() { new Ability(Abilities.GALVANIZE, 7) .attr(MoveTypeChangeAbAttr, Type.ELECTRIC, 1.2, (user, target, move) => move.type === Type.NORMAL && !move.hasAttr(VariableMoveTypeAttr)), new Ability(Abilities.SURGE_SURFER, 7) - .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), BattleStatMultiplierAbAttr, BattleStat.SPD, 2), + .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), StatMultiplierAbAttr, Stat.SPD, 2), new Ability(Abilities.SCHOOLING, 7) .attr(PostBattleInitFormChangeAbAttr, () => 0) .attr(PostSummonFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1) @@ -5480,9 +5475,9 @@ export function initAbilities() { .attr(FieldPriorityMoveImmunityAbAttr) .ignorable(), new Ability(Abilities.SOUL_HEART, 7) - .attr(PostKnockOutStatChangeAbAttr, BattleStat.SPATK, 1), + .attr(PostKnockOutStatStageChangeAbAttr, Stat.SPATK, 1), new Ability(Abilities.TANGLING_HAIR, 7) - .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), BattleStat.SPD, -1, false), + .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), Stat.SPD, -1, false), new Ability(Abilities.RECEIVER, 7) .attr(CopyFaintedAllyAbilityAbAttr) .attr(UncopiableAbilityAbAttr), @@ -5490,18 +5485,17 @@ export function initAbilities() { .attr(CopyFaintedAllyAbilityAbAttr) .attr(UncopiableAbilityAbAttr), new Ability(Abilities.BEAST_BOOST, 7) - .attr(PostVictoryStatChangeAbAttr, p => { - const battleStats = Utils.getEnumValues(BattleStat).slice(0, -3).map(s => s as BattleStat); - let highestBattleStat = 0; - let highestBattleStatIndex = 0; - battleStats.map((bs: BattleStat, i: integer) => { - const stat = p.getStat(bs + 1); - if (stat > highestBattleStat) { - highestBattleStatIndex = i; - highestBattleStat = stat; + .attr(PostVictoryStatStageChangeAbAttr, p => { + let highestStat: EffectiveStat; + let highestValue = 0; + for (const s of EFFECTIVE_STATS) { + const value = p.getStat(s, false); + if (value > highestValue) { + highestStat = s; + highestValue = value; } - }); - return highestBattleStatIndex; + } + return highestStat!; }, 1), new Ability(Abilities.RKS_SYSTEM, 7) .attr(UncopiableAbilityAbAttr) @@ -5530,10 +5524,10 @@ export function initAbilities() { //@ts-ignore .attr(MovePowerBoostAbAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2, 1.25), // TODO: fix TS issues new Ability(Abilities.INTREPID_SWORD, 8) - .attr(PostSummonStatChangeAbAttr, BattleStat.ATK, 1, true) + .attr(PostSummonStatStageChangeAbAttr, [ Stat.ATK ], 1, true) .condition(getOncePerBattleCondition(Abilities.INTREPID_SWORD)), new Ability(Abilities.DAUNTLESS_SHIELD, 8) - .attr(PostSummonStatChangeAbAttr, BattleStat.DEF, 1, true) + .attr(PostSummonStatStageChangeAbAttr, [ Stat.DEF ], 1, true) .condition(getOncePerBattleCondition(Abilities.DAUNTLESS_SHIELD)), new Ability(Abilities.LIBERO, 8) .attr(PokemonTypeChangeAbAttr), @@ -5542,7 +5536,7 @@ export function initAbilities() { .attr(FetchBallAbAttr) .condition(getOncePerBattleCondition(Abilities.BALL_FETCH)), new Ability(Abilities.COTTON_DOWN, 8) - .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, BattleStat.SPD, -1, false, true) + .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.SPD, -1, false, true) .bypassFaint(), new Ability(Abilities.PROPELLER_TAIL, 8) .attr(BlockRedirectAbAttr), @@ -5559,7 +5553,7 @@ export function initAbilities() { new Ability(Abilities.STALWART, 8) .attr(BlockRedirectAbAttr), new Ability(Abilities.STEAM_ENGINE, 8) - .attr(PostDefendStatChangeAbAttr, (target, user, move) => (move.type === Type.FIRE || move.type === Type.WATER) && move.category !== MoveCategory.STATUS, BattleStat.SPD, 6), + .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => (move.type === Type.FIRE || move.type === Type.WATER) && move.category !== MoveCategory.STATUS, Stat.SPD, 6), new Ability(Abilities.PUNK_ROCK, 8) .attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.SOUND_BASED), 1.3) .attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.hasFlag(MoveFlags.SOUND_BASED), 0.5) @@ -5630,26 +5624,26 @@ export function initAbilities() { new Ability(Abilities.UNSEEN_FIST, 8) .attr(IgnoreProtectOnContactAbAttr), new Ability(Abilities.CURIOUS_MEDICINE, 8) - .attr(PostSummonClearAllyStatsAbAttr), + .attr(PostSummonClearAllyStatStagesAbAttr), new Ability(Abilities.TRANSISTOR, 8) .attr(MoveTypePowerBoostAbAttr, Type.ELECTRIC), new Ability(Abilities.DRAGONS_MAW, 8) .attr(MoveTypePowerBoostAbAttr, Type.DRAGON), new Ability(Abilities.CHILLING_NEIGH, 8) - .attr(PostVictoryStatChangeAbAttr, BattleStat.ATK, 1), + .attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1), new Ability(Abilities.GRIM_NEIGH, 8) - .attr(PostVictoryStatChangeAbAttr, BattleStat.SPATK, 1), + .attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1), new Ability(Abilities.AS_ONE_GLASTRIER, 8) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneGlastrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(PreventBerryUseAbAttr) - .attr(PostVictoryStatChangeAbAttr, BattleStat.ATK, 1) + .attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(UnsuppressableAbilityAbAttr), new Ability(Abilities.AS_ONE_SPECTRIER, 8) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneSpectrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(PreventBerryUseAbAttr) - .attr(PostVictoryStatChangeAbAttr, BattleStat.SPATK, 1) + .attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(UnsuppressableAbilityAbAttr), @@ -5659,26 +5653,26 @@ export function initAbilities() { new Ability(Abilities.SEED_SOWER, 9) .attr(PostDefendTerrainChangeAbAttr, TerrainType.GRASSY), new Ability(Abilities.THERMAL_EXCHANGE, 9) - .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.type === Type.FIRE && move.category !== MoveCategory.STATUS, BattleStat.ATK, 1) + .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.type === Type.FIRE && move.category !== MoveCategory.STATUS, Stat.ATK, 1) .attr(StatusEffectImmunityAbAttr, StatusEffect.BURN) .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(PostDefendHpGatedStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [ Stat.ATK, Stat.SPATK, Stat.SPD ], 1) + .attr(PostDefendHpGatedStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [ Stat.DEF, Stat.SPDEF ], -1) .condition(getSheerForceHitDisableAbCondition()), new Ability(Abilities.PURIFYING_SALT, 9) .attr(StatusEffectImmunityAbAttr) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.GHOST, 0.5) .ignorable(), new Ability(Abilities.WELL_BAKED_BODY, 9) - .attr(TypeImmunityStatChangeAbAttr, Type.FIRE, BattleStat.DEF, 2) + .attr(TypeImmunityStatStageChangeAbAttr, Type.FIRE, Stat.DEF, 2) .ignorable(), new Ability(Abilities.WIND_RIDER, 9) - .attr(MoveImmunityStatChangeAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.hasFlag(MoveFlags.WIND_MOVE) && move.category !== MoveCategory.STATUS, BattleStat.ATK, 1) - .attr(PostSummonStatChangeOnArenaAbAttr, ArenaTagType.TAILWIND) + .attr(MoveImmunityStatStageChangeAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.hasFlag(MoveFlags.WIND_MOVE) && move.category !== MoveCategory.STATUS, Stat.ATK, 1) + .attr(PostSummonStatStageChangeOnArenaAbAttr, ArenaTagType.TAILWIND) .ignorable(), new Ability(Abilities.GUARD_DOG, 9) - .attr(PostIntimidateStatChangeAbAttr, [BattleStat.ATK], 1, true) + .attr(PostIntimidateStatStageChangeAbAttr, [Stat.ATK], 1, true) .attr(ForceSwitchOutImmunityAbAttr) .ignorable(), new Ability(Abilities.ROCKY_PAYLOAD, 9) @@ -5719,31 +5713,31 @@ export function initAbilities() { .ignorable() .partial(), new Ability(Abilities.VESSEL_OF_RUIN, 9) - .attr(FieldMultiplyBattleStatAbAttr, Stat.SPATK, 0.75) - .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonVesselOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: getStatName(Stat.SPATK) })) + .attr(FieldMultiplyStatAbAttr, Stat.SPATK, 0.75) + .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonVesselOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPATK)) })) .ignorable(), new Ability(Abilities.SWORD_OF_RUIN, 9) - .attr(FieldMultiplyBattleStatAbAttr, Stat.DEF, 0.75) - .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: getStatName(Stat.DEF) })) + .attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75) + .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })) .ignorable(), new Ability(Abilities.TABLETS_OF_RUIN, 9) - .attr(FieldMultiplyBattleStatAbAttr, Stat.ATK, 0.75) - .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: getStatName(Stat.ATK) })) + .attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75) + .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) })) .ignorable(), new Ability(Abilities.BEADS_OF_RUIN, 9) - .attr(FieldMultiplyBattleStatAbAttr, Stat.SPDEF, 0.75) - .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: getStatName(Stat.SPDEF) })) + .attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75) + .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })) .ignorable(), new Ability(Abilities.ORICHALCUM_PULSE, 9) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY) - .conditionalAttr(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN), BattleStatMultiplierAbAttr, BattleStat.ATK, 4 / 3), + .conditionalAttr(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.ATK, 4 / 3), new Ability(Abilities.HADRON_ENGINE, 9) .attr(PostSummonTerrainChangeAbAttr, TerrainType.ELECTRIC) .attr(PostBiomeChangeTerrainChangeAbAttr, TerrainType.ELECTRIC) - .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), BattleStatMultiplierAbAttr, BattleStat.SPATK, 4 / 3), + .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), StatMultiplierAbAttr, Stat.SPATK, 4 / 3), new Ability(Abilities.OPPORTUNIST, 9) - .attr(StatChangeCopyAbAttr), + .attr(StatStageChangeCopyAbAttr), new Ability(Abilities.CUD_CHEW, 9) .unimplemented(), new Ability(Abilities.SHARPNESS, 9) @@ -5769,11 +5763,11 @@ export function initAbilities() { .attr(MoveAbilityBypassAbAttr, (pokemon, move: Move) => move.category === MoveCategory.STATUS), new Ability(Abilities.MINDS_EYE, 9) .attr(IgnoreTypeImmunityAbAttr, Type.GHOST, [Type.NORMAL, Type.FIGHTING]) - .attr(ProtectStatAbAttr, BattleStat.ACC) - .attr(IgnoreOpponentEvasionAbAttr) + .attr(ProtectStatAbAttr, Stat.ACC) + .attr(IgnoreOpponentStatStagesAbAttr, [ Stat.EVA ]) .ignorable(), new Ability(Abilities.SUPERSWEET_SYRUP, 9) - .attr(PostSummonStatChangeAbAttr, BattleStat.EVA, -1) + .attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1) .condition(getOncePerBattleCondition(Abilities.SUPERSWEET_SYRUP)), new Ability(Abilities.HOSPITALITY, 9) .attr(PostSummonAllyHealAbAttr, 4, true) @@ -5781,25 +5775,25 @@ export function initAbilities() { new Ability(Abilities.TOXIC_CHAIN, 9) .attr(PostAttackApplyStatusEffectAbAttr, false, 30, StatusEffect.TOXIC), new Ability(Abilities.EMBODY_ASPECT_TEAL, 9) - .attr(PostBattleInitStatChangeAbAttr, BattleStat.SPD, 1, true) + .attr(PostBattleInitStatStageChangeAbAttr, [ Stat.SPD ], 1, true) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) .partial(), new Ability(Abilities.EMBODY_ASPECT_WELLSPRING, 9) - .attr(PostBattleInitStatChangeAbAttr, BattleStat.SPDEF, 1, true) + .attr(PostBattleInitStatStageChangeAbAttr, [ Stat.SPDEF ], 1, true) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) .partial(), new Ability(Abilities.EMBODY_ASPECT_HEARTHFLAME, 9) - .attr(PostBattleInitStatChangeAbAttr, BattleStat.ATK, 1, true) + .attr(PostBattleInitStatStageChangeAbAttr, [ Stat.ATK ], 1, true) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) .partial(), new Ability(Abilities.EMBODY_ASPECT_CORNERSTONE, 9) - .attr(PostBattleInitStatChangeAbAttr, BattleStat.DEF, 1, true) + .attr(PostBattleInitStatStageChangeAbAttr, [ Stat.DEF ], 1, true) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 09cc7a5b97c..fdc32b75c19 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -7,17 +7,17 @@ import Pokemon, { HitResult, PokemonMove } from "../field/pokemon"; import { StatusEffect } from "./status-effect"; import { BattlerIndex } from "../battle"; import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability"; -import { BattleStat } from "./battle-stat"; +import { Stat } from "#enums/stat"; import { CommonAnim, CommonBattleAnim } from "./battle-anims"; import i18next from "i18next"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Moves } from "#enums/moves"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase.js"; -import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js"; -import { ShowAbilityPhase } from "#app/phases/show-ability-phase.js"; -import { StatChangePhase } from "#app/phases/stat-change-phase.js"; +import { MoveEffectPhase } from "#app/phases/move-effect-phase"; +import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; +import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; export enum ArenaTagSide { BOTH, @@ -786,8 +786,8 @@ class StickyWebTag extends ArenaTrapTag { applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled); if (!cancelled.value) { pokemon.scene.queueMessage(i18next.t("arenaTag:stickyWebActivateTrap", { pokemonName: pokemon.getNameToRender() })); - const statLevels = new Utils.NumberHolder(-1); - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [BattleStat.SPD], statLevels.value)); + const stages = new Utils.NumberHolder(-1); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value)); } } @@ -875,7 +875,7 @@ class TailwindTag extends ArenaTag { // Raise attack by one stage if party member has WIND_RIDER ability if (pokemon.hasAbility(Abilities.WIND_RIDER)) { pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.getBattlerIndex())); - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK], 1, true)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.ATK ], 1, true)); } } } diff --git a/src/data/battle-stat.ts b/src/data/battle-stat.ts deleted file mode 100644 index a0cb7ca88e1..00000000000 --- a/src/data/battle-stat.ts +++ /dev/null @@ -1,71 +0,0 @@ -import i18next, { ParseKeys } from "i18next"; - -export enum BattleStat { - ATK, - DEF, - SPATK, - SPDEF, - SPD, - ACC, - EVA, - RAND, - HP -} - -export function getBattleStatName(stat: BattleStat) { - switch (stat) { - case BattleStat.ATK: - return i18next.t("pokemonInfo:Stat.ATK"); - case BattleStat.DEF: - return i18next.t("pokemonInfo:Stat.DEF"); - case BattleStat.SPATK: - return i18next.t("pokemonInfo:Stat.SPATK"); - case BattleStat.SPDEF: - return i18next.t("pokemonInfo:Stat.SPDEF"); - case BattleStat.SPD: - return i18next.t("pokemonInfo:Stat.SPD"); - case BattleStat.ACC: - return i18next.t("pokemonInfo:Stat.ACC"); - case BattleStat.EVA: - return i18next.t("pokemonInfo:Stat.EVA"); - case BattleStat.HP: - return i18next.t("pokemonInfo:Stat.HPStat"); - default: - return "???"; - } -} - -export function getBattleStatLevelChangeDescription(pokemonNameWithAffix: string, stats: string, levels: integer, up: boolean, count: number = 1) { - const stringKey = (() => { - if (up) { - switch (levels) { - case 1: - return "battle:statRose"; - case 2: - return "battle:statSharplyRose"; - case 3: - case 4: - case 5: - case 6: - return "battle:statRoseDrastically"; - default: - return "battle:statWontGoAnyHigher"; - } - } else { - switch (levels) { - case 1: - return "battle:statFell"; - case 2: - return "battle:statHarshlyFell"; - case 3: - case 4: - case 5: - case 6: - return "battle:statSeverelyFell"; - default: - return "battle:statWontGoAnyLower"; - } - } - })(); - return i18next.t(stringKey as ParseKeys, { pokemonNameWithAffix, stats, count }); -} diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 92df6fc294f..6e53ef00f45 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1,7 +1,6 @@ import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "./battle-anims"; import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { MoveResult, HitResult } from "../field/pokemon"; -import { Stat, getStatName } from "./pokemon-stat"; import { StatusEffect } from "./status-effect"; import * as Utils from "../utils"; import { ChargeAttr, MoveFlags, allMoves } from "./move"; @@ -9,20 +8,20 @@ import { Type } from "./type"; import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs } from "./ability"; import { TerrainType } from "./terrain"; import { WeatherType } from "./weather"; -import { BattleStat } from "./battle-stat"; import { allAbilities } from "./ability"; import { SpeciesFormChangeManualTrigger } from "./pokemon-forms"; import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import i18next from "#app/plugins/i18n.js"; -import { CommonAnimPhase } from "#app/phases/common-anim-phase.js"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase.js"; -import { MovePhase } from "#app/phases/move-phase.js"; -import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js"; -import { ShowAbilityPhase } from "#app/phases/show-ability-phase.js"; -import { StatChangePhase, StatChangeCallback } from "#app/phases/stat-change-phase.js"; +import i18next from "#app/plugins/i18n"; +import { Stat, type BattleStat, type EffectiveStat, EFFECTIVE_STATS, getStatKey } from "#app/enums/stat"; +import { CommonAnimPhase } from "#app/phases/common-anim-phase"; +import { MoveEffectPhase } from "#app/phases/move-effect-phase"; +import { MovePhase } from "#app/phases/move-phase"; +import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; +import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; +import { StatStageChangePhase, StatStageChangeCallback } from "#app/phases/stat-stage-change-phase"; export enum BattlerTagLapseType { FAINT, @@ -362,8 +361,8 @@ export class ConfusedTag extends BattlerTag { // 1/3 chance of hitting self with a 40 base power move if (pokemon.randSeedInt(3) === 0) { - const atk = pokemon.getBattleStat(Stat.ATK); - const def = pokemon.getBattleStat(Stat.DEF); + const atk = pokemon.getEffectiveStat(Stat.ATK); + const def = pokemon.getEffectiveStat(Stat.DEF); const damage = Utils.toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedInt(15, 85) / 100)); pokemon.scene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself")); pokemon.damageAndUpdate(damage); @@ -767,7 +766,7 @@ export class OctolockTag extends TrappedTag { const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType); if (shouldLapse) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [BattleStat.DEF, BattleStat.SPDEF], -1)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.DEF, Stat.SPDEF ], -1)); return true; } @@ -1093,7 +1092,7 @@ export class ContactDamageProtectedTag extends ProtectedTag { } } -export class ContactStatChangeProtectedTag extends ProtectedTag { +export class ContactStatStageChangeProtectedTag extends ProtectedTag { private stat: BattleStat; private levels: number; @@ -1110,7 +1109,7 @@ export class ContactStatChangeProtectedTag extends ProtectedTag { */ loadTag(source: BattlerTag | any): void { super.loadTag(source); - this.stat = source.stat as BattleStat; + this.stat = source.stat; this.levels = source.levels; } @@ -1121,7 +1120,7 @@ export class ContactStatChangeProtectedTag extends ProtectedTag { const effectPhase = pokemon.scene.getCurrentPhase(); if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) { const attacker = effectPhase.getPokemon(); - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, attacker.getBattlerIndex(), true, [ this.stat ], this.levels)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, attacker.getBattlerIndex(), true, [ this.stat ], this.levels)); } } @@ -1348,11 +1347,10 @@ export class HighestStatBoostTag extends AbilityBattlerTag { onAdd(pokemon: Pokemon): void { super.onAdd(pokemon); - const stats = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ]; - let highestStat: Stat; - stats.map(s => pokemon.getBattleStat(s)).reduce((highestValue: number, value: number, i: number) => { + let highestStat: EffectiveStat; + EFFECTIVE_STATS.map(s => pokemon.getEffectiveStat(s)).reduce((highestValue: number, value: number, i: number) => { if (value > highestValue) { - highestStat = stats[i]; + highestStat = EFFECTIVE_STATS[i]; return value; } return highestValue; @@ -1370,7 +1368,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag { break; } - pokemon.scene.queueMessage(i18next.t("battlerTags:highestStatBoostOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), statName: getStatName(highestStat) }), null, false, null, true); + pokemon.scene.queueMessage(i18next.t("battlerTags:highestStatBoostOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), statName: i18next.t(getStatKey(highestStat)) }), null, false, null, true); } onRemove(pokemon: Pokemon): void { @@ -1714,25 +1712,25 @@ export class IceFaceBlockDamageTag extends FormBlockDamageTag { */ export class StockpilingTag extends BattlerTag { public stockpiledCount: number = 0; - public statChangeCounts: { [BattleStat.DEF]: number; [BattleStat.SPDEF]: number } = { - [BattleStat.DEF]: 0, - [BattleStat.SPDEF]: 0 + public statChangeCounts: { [Stat.DEF]: number; [Stat.SPDEF]: number } = { + [Stat.DEF]: 0, + [Stat.SPDEF]: 0 }; constructor(sourceMove: Moves = Moves.NONE) { super(BattlerTagType.STOCKPILING, BattlerTagLapseType.CUSTOM, 1, sourceMove); } - private onStatsChanged: StatChangeCallback = (_, statsChanged, statChanges) => { - const defChange = statChanges[statsChanged.indexOf(BattleStat.DEF)] ?? 0; - const spDefChange = statChanges[statsChanged.indexOf(BattleStat.SPDEF)] ?? 0; + private onStatStagesChanged: StatStageChangeCallback = (_, statsChanged, statChanges) => { + const defChange = statChanges[statsChanged.indexOf(Stat.DEF)] ?? 0; + const spDefChange = statChanges[statsChanged.indexOf(Stat.SPDEF)] ?? 0; if (defChange) { - this.statChangeCounts[BattleStat.DEF]++; + this.statChangeCounts[Stat.DEF]++; } if (spDefChange) { - this.statChangeCounts[BattleStat.SPDEF]++; + this.statChangeCounts[Stat.SPDEF]++; } }; @@ -1740,8 +1738,8 @@ export class StockpilingTag extends BattlerTag { super.loadTag(source); this.stockpiledCount = source.stockpiledCount || 0; this.statChangeCounts = { - [ BattleStat.DEF ]: source.statChangeCounts?.[ BattleStat.DEF ] ?? 0, - [ BattleStat.SPDEF ]: source.statChangeCounts?.[ BattleStat.SPDEF ] ?? 0, + [ Stat.DEF ]: source.statChangeCounts?.[ Stat.DEF ] ?? 0, + [ Stat.SPDEF ]: source.statChangeCounts?.[ Stat.SPDEF ] ?? 0, }; } @@ -1761,9 +1759,9 @@ export class StockpilingTag extends BattlerTag { })); // Attempt to increase DEF and SPDEF by one stage, keeping track of successful changes. - pokemon.scene.unshiftPhase(new StatChangePhase( + pokemon.scene.unshiftPhase(new StatStageChangePhase( pokemon.scene, pokemon.getBattlerIndex(), true, - [BattleStat.SPDEF, BattleStat.DEF], 1, true, false, true, this.onStatsChanged + [Stat.SPDEF, Stat.DEF], 1, true, false, true, this.onStatStagesChanged )); } } @@ -1777,15 +1775,15 @@ export class StockpilingTag extends BattlerTag { * one stage for each stack which had successfully changed that particular stat during onAdd. */ onRemove(pokemon: Pokemon): void { - const defChange = this.statChangeCounts[BattleStat.DEF]; - const spDefChange = this.statChangeCounts[BattleStat.SPDEF]; + const defChange = this.statChangeCounts[Stat.DEF]; + const spDefChange = this.statChangeCounts[Stat.SPDEF]; if (defChange) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.DEF], -defChange, true, false, true)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.DEF ], -defChange, true, false, true)); } if (spDefChange) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.SPDEF], -spDefChange, true, false, true)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPDEF ], -spDefChange, true, false, true)); } } } @@ -1927,11 +1925,11 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source case BattlerTagType.SPIKY_SHIELD: return new ContactDamageProtectedTag(sourceMove, 8); case BattlerTagType.KINGS_SHIELD: - return new ContactStatChangeProtectedTag(sourceMove, tagType, BattleStat.ATK, -1); + return new ContactStatStageChangeProtectedTag(sourceMove, tagType, Stat.ATK, -1); case BattlerTagType.OBSTRUCT: - return new ContactStatChangeProtectedTag(sourceMove, tagType, BattleStat.DEF, -2); + return new ContactStatStageChangeProtectedTag(sourceMove, tagType, Stat.DEF, -2); case BattlerTagType.SILK_TRAP: - return new ContactStatChangeProtectedTag(sourceMove, tagType, BattleStat.SPD, -1); + return new ContactStatStageChangeProtectedTag(sourceMove, tagType, Stat.SPD, -1); case BattlerTagType.BANEFUL_BUNKER: return new ContactPoisonProtectedTag(sourceMove); case BattlerTagType.BURNING_BULWARK: diff --git a/src/data/berry.ts b/src/data/berry.ts index d0c9c311e16..01325ee39dd 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -1,14 +1,14 @@ import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { HitResult } from "../field/pokemon"; -import { BattleStat } from "./battle-stat"; import { getStatusEffectHealText } from "./status-effect"; import * as Utils from "../utils"; import { DoubleBerryEffectAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs } from "./ability"; import i18next from "i18next"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; -import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js"; -import { StatChangePhase } from "#app/phases/stat-change-phase.js"; +import { Stat, type BattleStat } from "#app/enums/stat"; +import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; export function getBerryName(berryType: BerryType): string { return i18next.t(`berry:${BerryType[berryType]}.name`); @@ -35,9 +35,10 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate { case BerryType.SALAC: return (pokemon: Pokemon) => { const threshold = new Utils.NumberHolder(0.25); - const battleStat = (berryType - BerryType.LIECHI) as BattleStat; + // Offset BerryType such that LIECHI -> Stat.ATK = 1, GANLON -> Stat.DEF = 2, so on and so forth + const stat: BattleStat = berryType - BerryType.ENIGMA; applyAbAttrs(ReduceBerryUseThresholdAbAttr, pokemon, null, false, threshold); - return pokemon.getHpRatio() < threshold.value && pokemon.summonData.battleStats[battleStat] < 6; + return pokemon.getHpRatio() < threshold.value && pokemon.getStatStage(stat) < 6; }; case BerryType.LANSAT: return (pokemon: Pokemon) => { @@ -95,10 +96,11 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { if (pokemon.battleData) { pokemon.battleData.berriesEaten.push(berryType); } - const battleStat = (berryType - BerryType.LIECHI) as BattleStat; - const statLevels = new Utils.NumberHolder(1); - applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statLevels); - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ battleStat ], statLevels.value)); + // Offset BerryType such that LIECHI -> Stat.ATK = 1, GANLON -> Stat.DEF = 2, so on and so forth + const stat: BattleStat = berryType - BerryType.ENIGMA; + const statStages = new Utils.NumberHolder(1); + applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statStages); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], statStages.value)); }; case BerryType.LANSAT: return (pokemon: Pokemon) => { @@ -112,9 +114,10 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { if (pokemon.battleData) { pokemon.battleData.berriesEaten.push(berryType); } - const statLevels = new Utils.NumberHolder(2); - applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statLevels); - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ BattleStat.RAND ], statLevels.value)); + const randStat = Utils.randSeedInt(Stat.SPD, Stat.ATK); + const stages = new Utils.NumberHolder(2); + applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, stages); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ randStat ], stages.value)); }; case BerryType.LEPPA: return (pokemon: Pokemon) => { diff --git a/src/data/move.ts b/src/data/move.ts index 1dc715f264a..bcdb16cdfbc 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1,5 +1,4 @@ import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims"; -import { BattleStat, getBattleStatName } from "./battle-stat"; import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, TrappedTag, TypeBoostTag } from "./battler-tags"; import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon"; @@ -13,7 +12,6 @@ import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilit import { allAbilities } from "./ability"; import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, PokemonMoveAccuracyBoosterModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier"; import { BattlerIndex, BattleType } from "../battle"; -import { Stat } from "./pokemon-stat"; import { TerrainType } from "./terrain"; import { ModifierPoolType } from "#app/modifier/modifier-type"; import { Command } from "../ui/command-ui-handler"; @@ -27,13 +25,14 @@ import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { MoveUsedEvent } from "#app/events/battle-scene"; +import { Stat, type BattleStat, type EffectiveStat, BATTLE_STATS, EFFECTIVE_STATS, getStatKey } from "#app/enums/stat"; import { PartyStatusCurePhase } from "#app/phases/party-status-cure-phase"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { MovePhase } from "#app/phases/move-phase"; import { NewBattlePhase } from "#app/phases/new-battle-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; -import { StatChangePhase } from "#app/phases/stat-change-phase"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { SwitchPhase } from "#app/phases/switch-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { SpeciesFormChangeRevertWeatherFormTrigger } from "./pokemon-forms"; @@ -819,10 +818,10 @@ export class AttackMove extends Move { attackScore = Math.pow(effectiveness - 1, 2) * effectiveness < 1 ? -2 : 2; if (attackScore) { if (this.category === MoveCategory.PHYSICAL) { - const atk = new Utils.IntegerHolder(user.getBattleStat(Stat.ATK, target)); + const atk = new Utils.IntegerHolder(user.getEffectiveStat(Stat.ATK, target)); applyMoveAttrs(VariableAtkAttr, user, target, move, atk); - if (atk.value > user.getBattleStat(Stat.SPATK, target)) { - const statRatio = user.getBattleStat(Stat.SPATK, target) / atk.value; + if (atk.value > user.getEffectiveStat(Stat.SPATK, target)) { + const statRatio = user.getEffectiveStat(Stat.SPATK, target) / atk.value; if (statRatio <= 0.75) { attackScore *= 2; } else if (statRatio <= 0.875) { @@ -830,10 +829,10 @@ export class AttackMove extends Move { } } } else { - const spAtk = new Utils.IntegerHolder(user.getBattleStat(Stat.SPATK, target)); + const spAtk = new Utils.IntegerHolder(user.getEffectiveStat(Stat.SPATK, target)); applyMoveAttrs(VariableAtkAttr, user, target, move, spAtk); - if (spAtk.value > user.getBattleStat(Stat.ATK, target)) { - const statRatio = user.getBattleStat(Stat.ATK, target) / spAtk.value; + if (spAtk.value > user.getEffectiveStat(Stat.ATK, target)) { + const statRatio = user.getEffectiveStat(Stat.ATK, target) / spAtk.value; if (statRatio <= 0.75) { attackScore *= 2; } else if (statRatio <= 0.875) { @@ -1100,9 +1099,9 @@ export class PreMoveMessageAttr extends MoveAttr { */ export class RespectAttackTypeImmunityAttr extends MoveAttr { } -export class IgnoreOpponentStatChangesAttr extends MoveAttr { +export class IgnoreOpponentStatStagesAttr extends MoveAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - (args[0] as Utils.IntegerHolder).value = 0; + (args[0] as Utils.BooleanHolder).value = true; return true; } @@ -1730,10 +1729,9 @@ export class HealOnAllyAttr extends HealAttr { */ export class HitHealAttr extends MoveEffectAttr { private healRatio: number; - private message: string; - private healStat: Stat | null; + private healStat: EffectiveStat | null; - constructor(healRatio?: number | null, healStat?: Stat) { + constructor(healRatio?: number | null, healStat?: EffectiveStat) { super(true, MoveEffectTrigger.HIT); this.healRatio = healRatio ?? 0.5; @@ -1755,7 +1753,7 @@ export class HitHealAttr extends MoveEffectAttr { const reverseDrain = target.hasAbilityWithAttr(ReverseDrainAbAttr, false); if (this.healStat !== null) { // Strength Sap formula - healAmount = target.getBattleStat(this.healStat); + healAmount = target.getEffectiveStat(this.healStat); message = i18next.t("battle:drainMessage", {pokemonName: getPokemonNameWithAffix(target)}); } else { // Default healing formula used by draining moves like Absorb, Draining Kiss, Bitter Blade, etc. @@ -1785,7 +1783,7 @@ export class HitHealAttr extends MoveEffectAttr { */ getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { if (this.healStat) { - const healAmount = target.getBattleStat(this.healStat); + const healAmount = target.getEffectiveStat(this.healStat); return Math.floor(Math.max(0, (Math.min(1, (healAmount+user.hp)/user.getMaxHp() - 0.33))) / user.getHpRatio()); } return Math.floor(Math.max((1 - user.getHpRatio()) - 0.33, 0) * (move.power / 4)); @@ -2516,14 +2514,14 @@ export class ElectroShotChargeAttr extends ChargeAttr { const weatherType = user.scene.arena.weather?.weatherType; if (!user.scene.arena.weather?.isEffectSuppressed(user.scene) && (weatherType === WeatherType.RAIN || weatherType === WeatherType.HEAVY_RAIN)) { // Apply the SPATK increase every call when used in the rain - const statChangeAttr = new StatChangeAttr(BattleStat.SPATK, 1, true); + const statChangeAttr = new StatStageChangeAttr([ Stat.SPATK ], 1, true); statChangeAttr.apply(user, target, move, args); // After the SPATK is raised, execute the move resolution e.g. deal damage resolve(false); } else { if (!this.statIncreaseApplied) { // Apply the SPATK increase only if it hasn't been applied before e.g. on the first turn charge up animation - const statChangeAttr = new StatChangeAttr(BattleStat.SPATK, 1, true); + const statChangeAttr = new StatStageChangeAttr([ Stat.SPATK ], 1, true); statChangeAttr.apply(user, target, move, args); // Set the flag to true so that on the following turn it doesn't raise SPATK a second time this.statIncreaseApplied = true; @@ -2571,18 +2569,16 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr { } } -export class StatChangeAttr extends MoveEffectAttr { +export class StatStageChangeAttr extends MoveEffectAttr { public stats: BattleStat[]; - public levels: integer; + public stages: integer; private condition: MoveConditionFunc | null; private showMessage: boolean; - constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false) { + constructor(stats: BattleStat[], stages: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false) { super(selfTarget, moveEffectTrigger, firstHitOnly, false, firstTargetOnly); - this.stats = typeof(stats) === "number" - ? [ stats as BattleStat ] - : stats as BattleStat[]; - this.levels = levels; + this.stats = stats; + this.stages = stages; this.condition = condition!; // TODO: is this bang correct? this.showMessage = showMessage; } @@ -2594,8 +2590,8 @@ export class StatChangeAttr extends MoveEffectAttr { const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true); 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)); + const stages = this.getLevels(user); + user.scene.unshiftPhase(new StatStageChangePhase(user.scene, (this.selfTarget ? user : target).getBattlerIndex(), this.selfTarget, this.stats, stages, this.showMessage)); return true; } @@ -2603,7 +2599,7 @@ export class StatChangeAttr extends MoveEffectAttr { } getLevels(_user: Pokemon): integer { - return this.levels; + return this.stages; } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { @@ -2611,29 +2607,30 @@ export class StatChangeAttr extends MoveEffectAttr { const moveLevels = this.getLevels(user); for (const stat of this.stats) { let levels = moveLevels; + const statStage = target.getStatStage(stat); if (levels > 0) { - levels = Math.min(target.summonData.battleStats[stat] + levels, 6) - target.summonData.battleStats[stat]; + levels = Math.min(statStage + levels, 6) - statStage; } else { - levels = Math.max(target.summonData.battleStats[stat] + levels, -6) - target.summonData.battleStats[stat]; + levels = Math.max(statStage + levels, -6) - statStage; } let noEffect = false; switch (stat) { - case BattleStat.ATK: + case Stat.ATK: if (this.selfTarget) { noEffect = !user.getMoveset().find(m => m instanceof AttackMove && m.category === MoveCategory.PHYSICAL); } break; - case BattleStat.DEF: + case Stat.DEF: if (!this.selfTarget) { noEffect = !user.getMoveset().find(m => m instanceof AttackMove && m.category === MoveCategory.PHYSICAL); } break; - case BattleStat.SPATK: + case Stat.SPATK: if (this.selfTarget) { noEffect = !user.getMoveset().find(m => m instanceof AttackMove && m.category === MoveCategory.SPECIAL); } break; - case BattleStat.SPDEF: + case Stat.SPDEF: if (!this.selfTarget) { noEffect = !user.getMoveset().find(m => m instanceof AttackMove && m.category === MoveCategory.SPECIAL); } @@ -2648,18 +2645,16 @@ export class StatChangeAttr extends MoveEffectAttr { } } -export class PostVictoryStatChangeAttr extends MoveAttr { +export class PostVictoryStatStageChangeAttr extends MoveAttr { private stats: BattleStat[]; - private levels: integer; + private stages: number; private condition: MoveConditionFunc | null; private showMessage: boolean; - constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean, condition?: MoveConditionFunc, showMessage: boolean = true, firstHitOnly: boolean = false) { + constructor(stats: BattleStat[], stages: number, selfTarget?: boolean, condition?: MoveConditionFunc, showMessage: boolean = true, firstHitOnly: boolean = false) { super(); - this.stats = typeof(stats) === "number" - ? [ stats as BattleStat ] - : stats as BattleStat[]; - this.levels = levels; + this.stats = stats; + this.stages = stages; this.condition = condition!; // TODO: is this bang correct? this.showMessage = showMessage; } @@ -2667,49 +2662,48 @@ export class PostVictoryStatChangeAttr extends MoveAttr { if (this.condition && !this.condition(user, target, move)) { return; } - const statChangeAttr = new StatChangeAttr(this.stats, this.levels, this.showMessage); + const statChangeAttr = new StatStageChangeAttr(this.stats, this.stages, this.showMessage); statChangeAttr.apply(user, target, move); } } -export class AcupressureStatChangeAttr extends MoveEffectAttr { +export class AcupressureStatStageChangeAttr extends MoveEffectAttr { constructor() { super(); } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise { - let randStats = [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD, BattleStat.ACC, BattleStat.EVA ]; - randStats = randStats.filter(s => target.summonData.battleStats[s] < 6); + const randStats = BATTLE_STATS.filter(s => target.getStatStage(s) < 6); if (randStats.length > 0) { const boostStat = [randStats[Utils.randInt(randStats.length)]]; - user.scene.unshiftPhase(new StatChangePhase(user.scene, target.getBattlerIndex(), this.selfTarget, boostStat, 2)); + user.scene.unshiftPhase(new StatStageChangePhase(user.scene, target.getBattlerIndex(), this.selfTarget, boostStat, 2)); return true; } return false; } } -export class GrowthStatChangeAttr extends StatChangeAttr { +export class GrowthStatStageChangeAttr extends StatStageChangeAttr { constructor() { - super([ BattleStat.ATK, BattleStat.SPATK ], 1, true); + super([ Stat.ATK, Stat.SPATK ], 1, true); } getLevels(user: Pokemon): number { if (!user.scene.arena.weather?.isEffectSuppressed(user.scene)) { const weatherType = user.scene.arena.weather?.weatherType; if (weatherType === WeatherType.SUNNY || weatherType === WeatherType.HARSH_SUN) { - return this.levels + 1; + return this.stages + 1; } } - return this.levels; + return this.stages; } } -export class CutHpStatBoostAttr extends StatChangeAttr { +export class CutHpStatStageBoostAttr extends StatStageChangeAttr { private cutRatio: integer; private messageCallback: ((user: Pokemon) => void) | undefined; - constructor(stat: BattleStat | BattleStat[], levels: integer, cutRatio: integer, messageCallback?: ((user: Pokemon) => void) | undefined) { + constructor(stat: BattleStat[], levels: integer, cutRatio: integer, messageCallback?: ((user: Pokemon) => void) | undefined) { super(stat, levels, true, null, true); this.cutRatio = cutRatio; @@ -2730,7 +2724,7 @@ export class CutHpStatBoostAttr extends StatChangeAttr { } getCondition(): MoveConditionFunc { - return (user, target, move) => user.getHpRatio() > 1 / this.cutRatio && this.stats.some(s => user.summonData.battleStats[s] < 6); + return (user, _target, _move) => user.getHpRatio() > 1 / this.cutRatio && this.stats.some(s => user.getStatStage(s) < 6); } } @@ -2740,9 +2734,11 @@ export class CopyStatsAttr extends MoveEffectAttr { return false; } - for (let s = 0; s < target.summonData.battleStats.length; s++) { - user.summonData.battleStats[s] = target.summonData.battleStats[s]; + // Copy all stat stages + for (const s of BATTLE_STATS) { + user.setStatStage(s, target.getStatStage(s)); } + if (target.getTag(BattlerTagType.CRIT_BOOST)) { user.addTag(BattlerTagType.CRIT_BOOST, 0, move.id); } else { @@ -2762,9 +2758,10 @@ export class InvertStatsAttr extends MoveEffectAttr { return false; } - for (let s = 0; s < target.summonData.battleStats.length; s++) { - target.summonData.battleStats[s] *= -1; + for (const s of BATTLE_STATS) { + target.setStatStage(s, -target.getStatStage(s)); } + target.updateInfo(); user.updateInfo(); @@ -2798,39 +2795,61 @@ export class ResetStatsAttr extends MoveEffectAttr { } resetStats(pokemon: Pokemon) { - for (let s = 0; s < pokemon.summonData.battleStats.length; s++) { - pokemon.summonData.battleStats[s] = 0; + for (const s of BATTLE_STATS) { + pokemon.setStatStage(s, 0); } pokemon.updateInfo(); } } /** - * Attribute used for moves which swap the user and the target's stat changes. + * Attribute used for status moves, specifically Heart, Guard, and Power Swap, + * that swaps the user's and target's corresponding stat stages. + * @extends MoveEffectAttr + * @see {@linkcode apply} */ -export class SwapStatsAttr extends MoveEffectAttr { +export class SwapStatStagesAttr extends MoveEffectAttr { + /** The stat stages to be swapped between the user and the target */ + private stats: readonly BattleStat[]; + + constructor(stats: readonly BattleStat[]) { + super(); + + this.stats = stats; + } + /** - * Swaps the user and the target's stat changes. - * @param user Pokemon that used the move - * @param target The target of the move - * @param move Move with this attribute + * For all {@linkcode stats}, swaps the user's and target's corresponding stat + * stage. + * @param user the {@linkcode Pokemon} that used the move + * @param target the {@linkcode Pokemon} that the move was used on + * @param move N/A * @param args N/A - * @returns true if the function succeeds + * @returns true if attribute application succeeds */ apply(user: Pokemon, target: Pokemon, move: Move, args: any []): boolean { - if (!super.apply(user, target, move, args)) { - return false; - } //Exits if the move can't apply - let priorBoost : integer; //For storing a stat boost - for (let s = 0; s < target.summonData.battleStats.length; s++) { - priorBoost = user.summonData.battleStats[s]; //Store user stat boost - user.summonData.battleStats[s] = target.summonData.battleStats[s]; //Applies target boost to self - target.summonData.battleStats[s] = priorBoost; //Applies stored boost to target + if (super.apply(user, target, move, args)) { + for (const s of BATTLE_STATS) { + const temp = user.getStatStage(s); + user.setStatStage(s, target.getStatStage(s)); + target.setStatStage(s, temp); + } + + target.updateInfo(); + user.updateInfo(); + + if (this.stats.length === 7) { + user.scene.queueMessage(i18next.t("moveTriggers:switchedStatChanges", { pokemonName: getPokemonNameWithAffix(user) })); + } else if (this.stats.length === 2) { + user.scene.queueMessage(i18next.t("moveTriggers:switchedTwoStatChanges", { + pokemonName: getPokemonNameWithAffix(user), + firstStat: i18next.t(getStatKey(this.stats[0])), + secondStat: i18next.t(getStatKey(this.stats[1])) + })); + } + return true; } - target.updateInfo(); - user.updateInfo(); - target.scene.queueMessage(i18next.t("moveTriggers:switchedStatChanges", {pokemonName: getPokemonNameWithAffix(user)})); - return true; + return false; } } @@ -3075,7 +3094,7 @@ export class WeightPowerAttr extends VariablePowerAttr { **/ export class ElectroBallPowerAttr extends VariablePowerAttr { /** - * Move that deals more damage the faster {@linkcode BattleStat.SPD} + * Move that deals more damage the faster {@linkcode Stat.SPD} * the user is compared to the target. * @param user Pokemon that used the move * @param target The target of the move @@ -3086,7 +3105,7 @@ export class ElectroBallPowerAttr extends VariablePowerAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const power = args[0] as Utils.NumberHolder; - const statRatio = target.getBattleStat(Stat.SPD) / user.getBattleStat(Stat.SPD); + const statRatio = target.getEffectiveStat(Stat.SPD) / user.getEffectiveStat(Stat.SPD); const statThresholds = [ 0.25, 1 / 3, 0.5, 1, -1 ]; const statThresholdPowers = [ 150, 120, 80, 60, 40 ]; @@ -3110,7 +3129,7 @@ export class ElectroBallPowerAttr extends VariablePowerAttr { **/ export class GyroBallPowerAttr extends VariablePowerAttr { /** - * Move that deals more damage the slower {@linkcode BattleStat.SPD} + * Move that deals more damage the slower {@linkcode Stat.SPD} * the user is compared to the target. * @param user Pokemon that used the move * @param target The target of the move @@ -3120,14 +3139,14 @@ export class GyroBallPowerAttr extends VariablePowerAttr { */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const power = args[0] as Utils.NumberHolder; - const userSpeed = user.getBattleStat(Stat.SPD); + const userSpeed = user.getEffectiveStat(Stat.SPD); if (userSpeed < 1) { // Gen 6+ always have 1 base power power.value = 1; return true; } - power.value = Math.floor(Math.min(150, 25 * target.getBattleStat(Stat.SPD) / userSpeed + 1)); + power.value = Math.floor(Math.min(150, 25 * target.getEffectiveStat(Stat.SPD) / userSpeed + 1)); return true; } } @@ -3347,18 +3366,18 @@ export class HitCountPowerAttr extends VariablePowerAttr { } /** - * Turning a once was (StatChangeCountPowerAttr) statement and making it available to call for any attribute. - * @param {Pokemon} pokemon The pokemon that is being used to calculate the count of positive stats - * @returns {number} Returns the amount of positive stats + * Tallies the number of positive stages for a given {@linkcode Pokemon}. + * @param pokemon The {@linkcode Pokemon} that is being used to calculate the count of positive stats + * @returns the amount of positive stats */ -const countPositiveStats = (pokemon: Pokemon): number => { - return pokemon.summonData.battleStats.reduce((total, stat) => (stat && stat > 0) ? total + stat : total, 0); +const countPositiveStatStages = (pokemon: Pokemon): number => { + return pokemon.getStatStages().reduce((total, stat) => (stat && stat > 0) ? total + stat : total, 0); }; /** - * Attribute that increases power based on the amount of positive stat increases. + * Attribute that increases power based on the amount of positive stat stage increases. */ -export class StatChangeCountPowerAttr extends VariablePowerAttr { +export class PositiveStatStagePowerAttr extends VariablePowerAttr { /** * @param {Pokemon} user The pokemon that is being used to calculate the amount of positive stats @@ -3368,9 +3387,9 @@ export class StatChangeCountPowerAttr extends VariablePowerAttr { * @returns {boolean} Returns true if attribute is applied */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const positiveStats: number = countPositiveStats(user); + const positiveStatStages: number = countPositiveStatStages(user); - (args[0] as Utils.NumberHolder).value += positiveStats * 20; + (args[0] as Utils.NumberHolder).value += positiveStatStages * 20; return true; } } @@ -3392,10 +3411,10 @@ export class PunishmentPowerAttr extends VariablePowerAttr { * @returns Returns true if attribute is applied */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const positiveStats: number = countPositiveStats(target); + const positiveStatStages: number = countPositiveStatStages(target); (args[0] as Utils.NumberHolder).value = Math.min( this.PUNISHMENT_MAX_BASE_POWER, - this.PUNISHMENT_MIN_BASE_POWER + positiveStats * 20 + this.PUNISHMENT_MIN_BASE_POWER + positiveStatStages * 20 ); return true; } @@ -3615,7 +3634,7 @@ export class TargetAtkUserAtkAttr extends VariableAtkAttr { super(); } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - (args[0] as Utils.IntegerHolder).value = target.getBattleStat(Stat.ATK, target); + (args[0] as Utils.IntegerHolder).value = target.getEffectiveStat(Stat.ATK, target); return true; } } @@ -3626,7 +3645,7 @@ export class DefAtkAttr extends VariableAtkAttr { } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - (args[0] as Utils.IntegerHolder).value = user.getBattleStat(Stat.DEF, target); + (args[0] as Utils.IntegerHolder).value = user.getEffectiveStat(Stat.DEF, target); return true; } } @@ -3648,7 +3667,7 @@ export class DefDefAttr extends VariableDefAttr { } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - (args[0] as Utils.IntegerHolder).value = target.getBattleStat(Stat.DEF, user); + (args[0] as Utils.IntegerHolder).value = target.getEffectiveStat(Stat.DEF, user); return true; } } @@ -3770,7 +3789,7 @@ export class PhotonGeyserCategoryAttr extends VariableMoveCategoryAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const category = (args[0] as Utils.NumberHolder); - if (user.getBattleStat(Stat.ATK, target, move) > user.getBattleStat(Stat.SPATK, target, move)) { + if (user.getEffectiveStat(Stat.ATK, target, move) > user.getEffectiveStat(Stat.SPATK, target, move)) { category.value = MoveCategory.PHYSICAL; return true; } @@ -3783,7 +3802,7 @@ export class TeraBlastCategoryAttr extends VariableMoveCategoryAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const category = (args[0] as Utils.NumberHolder); - if (user.isTerastallized() && user.getBattleStat(Stat.ATK, target, move) > user.getBattleStat(Stat.SPATK, target, move)) { + if (user.isTerastallized() && user.getEffectiveStat(Stat.ATK, target, move) > user.getEffectiveStat(Stat.SPATK, target, move)) { category.value = MoveCategory.PHYSICAL; return true; } @@ -3847,8 +3866,8 @@ export class StatusCategoryOnAllyAttr extends VariableMoveCategoryAttr { export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const category = (args[0] as Utils.IntegerHolder); - const atkRatio = user.getBattleStat(Stat.ATK, target, move) / target.getBattleStat(Stat.DEF, user, move); - const specialRatio = user.getBattleStat(Stat.SPATK, target, move) / target.getBattleStat(Stat.SPDEF, user, move); + const atkRatio = user.getEffectiveStat(Stat.ATK, target, move) / target.getEffectiveStat(Stat.DEF, user, move); + const specialRatio = user.getEffectiveStat(Stat.SPATK, target, move) / target.getEffectiveStat(Stat.SPDEF, user, move); // Shell Side Arm is much more complicated than it looks, this is a partial implementation to try to achieve something similar to the games if (atkRatio > specialRatio) { @@ -4605,8 +4624,8 @@ export class CurseAttr extends MoveEffectAttr { target.addTag(BattlerTagType.CURSED, 0, move.id, user.id); return true; } else { - user.scene.unshiftPhase(new StatChangePhase(user.scene, user.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF], 1)); - user.scene.unshiftPhase(new StatChangePhase(user.scene, user.getBattlerIndex(), true, [BattleStat.SPD], -1)); + user.scene.unshiftPhase(new StatStageChangePhase(user.scene, user.getBattlerIndex(), true, [ Stat.ATK, Stat.DEF], 1)); + user.scene.unshiftPhase(new StatStageChangePhase(user.scene, user.getBattlerIndex(), true, [ Stat.SPD ], -1)); return true; } } @@ -5163,8 +5182,8 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } let ret = this.user ? Math.floor((1 - user.getHpRatio()) * 20) : super.getUserBenefitScore(user, target, move); if (this.user && this.batonPass) { - const battleStatTotal = user.summonData.battleStats.reduce((bs: integer, total: integer) => total += bs, 0); - ret = ret / 2 + (Phaser.Tweens.Builders.GetEaseFunction("Sine.easeOut")(Math.min(Math.abs(battleStatTotal), 10) / 10) * (battleStatTotal >= 0 ? 10 : -10)); + const statStageTotal = user.getStatStages().reduce((s: integer, total: integer) => total += s, 0); + ret = ret / 2 + (Phaser.Tweens.Builders.GetEaseFunction("Sine.easeOut")(Math.min(Math.abs(statStageTotal), 10) / 10) * (statStageTotal >= 0 ? 10 : -10)); } return ret; } @@ -5989,8 +6008,17 @@ export class TransformAttr extends MoveEffectAttr { user.summonData.ability = target.getAbility().id; user.summonData.gender = target.getGender(); user.summonData.fusionGender = target.getFusionGender(); - user.summonData.stats = [ user.stats[Stat.HP] ].concat(target.stats.slice(1)); - user.summonData.battleStats = target.summonData.battleStats.slice(0); + + // Copy all stats (except HP) + for (const s of EFFECTIVE_STATS) { + user.setStat(s, target.getStat(s, false), false); + } + + // Copy all stat stages + for (const s of BATTLE_STATS) { + user.setStatStage(s, target.getStatStage(s)); + } + user.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m?.moveId!, m?.ppUsed, m?.ppUp)); // TODO: is this bang correct? user.summonData.types = target.getTypes(); @@ -5998,12 +6026,102 @@ export class TransformAttr extends MoveEffectAttr { user.loadAssets(false).then(() => { user.playAnim(); + user.updateInfo(); resolve(true); }); }); } } +/** + * Attribute used for status moves, namely Speed Swap, + * that swaps the user's and target's corresponding stats. + * @extends MoveEffectAttr + * @see {@linkcode apply} + */ +export class SwapStatAttr extends MoveEffectAttr { + /** The stat to be swapped between the user and the target */ + private stat: EffectiveStat; + + constructor(stat: EffectiveStat) { + super(); + + this.stat = stat; + } + + /** + * Takes the average of the user's and target's corresponding current + * {@linkcode stat} values and sets that stat to the average for both + * temporarily. + * @param user the {@linkcode Pokemon} that used the move + * @param target the {@linkcode Pokemon} that the move was used on + * @param move N/A + * @param args N/A + * @returns true if attribute application succeeds + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (super.apply(user, target, move, args)) { + const temp = user.getStat(this.stat, false); + user.setStat(this.stat, target.getStat(this.stat, false), false); + target.setStat(this.stat, temp, false); + + user.scene.queueMessage(i18next.t("moveTriggers:switchedStat", { + pokemonName: getPokemonNameWithAffix(user), + stat: i18next.t(getStatKey(this.stat)), + })); + + return true; + } + return false; + } +} + +/** + * Attribute used for status moves, namely Power Split and Guard Split, + * that take the average of a user's and target's corresponding + * stats and assign that average back to each corresponding stat. + * @extends MoveEffectAttr + * @see {@linkcode apply} + */ +export class AverageStatsAttr extends MoveEffectAttr { + /** The stats to be averaged individually between the user and the target */ + private stats: readonly EffectiveStat[]; + private msgKey: string; + + constructor(stats: readonly EffectiveStat[], msgKey: string) { + super(); + + this.stats = stats; + this.msgKey = msgKey; + } + + /** + * Takes the average of the user's and target's corresponding {@linkcode stat} + * values and sets those stats to the corresponding average for both + * temporarily. + * @param user the {@linkcode Pokemon} that used the move + * @param target the {@linkcode Pokemon} that the move was used on + * @param move N/A + * @param args N/A + * @returns true if attribute application succeeds + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (super.apply(user, target, move, args)) { + for (const s of this.stats) { + const avg = Math.floor((user.getStat(s, false) + target.getStat(s, false)) / 2); + + user.setStat(s, avg, false); + target.setStat(s, avg, false); + } + + user.scene.queueMessage(i18next.t(this.msgKey, { pokemonName: getPokemonNameWithAffix(user) })); + + return true; + } + return false; + } +} + export class DiscourageFrequentUseAttr extends MoveAttr { getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { const lastMoves = user.getLastXMoves(4); @@ -6072,7 +6190,7 @@ export class AddBattlerTagIfBoostedAttr extends AddBattlerTagAttr { * @returns true */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (target.turnData.battleStatsIncreased) { + if (target.turnData.statStagesIncreased) { super.apply(user, target, move, args); } return true; @@ -6099,7 +6217,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr { * @returns true */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (target.turnData.battleStatsIncreased) { + if (target.turnData.statStagesIncreased) { target.trySetStatus(this.effect, true, user); } return true; @@ -6457,7 +6575,7 @@ export function initMoves() { .ignoresVirtual() .target(MoveTarget.ALL_NEAR_ENEMIES), new SelfStatusMove(Moves.SWORDS_DANCE, Type.NORMAL, -1, 20, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.ATK, 2, true) + .attr(StatStageChangeAttr, [ Stat.ATK ], 2, true) .danceMove(), new AttackMove(Moves.CUT, Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, 0, 1) .slicingMove(), @@ -6493,7 +6611,7 @@ export function initMoves() { new AttackMove(Moves.ROLLING_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 85, 15, 30, 0, 1) .attr(FlinchAttr), new StatusMove(Moves.SAND_ATTACK, Type.GROUND, 100, 15, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.ACC, -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1), new AttackMove(Moves.HEADBUTT, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 15, 30, 0, 1) .attr(FlinchAttr), new AttackMove(Moves.HORN_ATTACK, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 25, -1, 0, 1), @@ -6521,7 +6639,7 @@ export function initMoves() { .attr(RecoilAttr, false, 0.33) .recklessMove(), new StatusMove(Moves.TAIL_WHIP, Type.NORMAL, 100, 30, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.DEF, -1) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.POISON_STING, Type.POISON, MoveCategory.PHYSICAL, 15, 100, 35, 30, 0, 1) .attr(StatusEffectAttr, StatusEffect.POISON) @@ -6534,13 +6652,13 @@ export function initMoves() { .attr(MultiHitAttr) .makesContact(false), new StatusMove(Moves.LEER, Type.NORMAL, 100, 30, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.DEF, -1) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.BITE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, 30, 0, 1) .attr(FlinchAttr) .bitingMove(), new StatusMove(Moves.GROWL, Type.NORMAL, 100, 40, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.ATK, -1) + .attr(StatStageChangeAttr, [ Stat.ATK ], -1) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.ROAR, Type.NORMAL, -1, 20, -1, -6, 1) @@ -6559,7 +6677,7 @@ export function initMoves() { .attr(DisableMoveAttr) .condition(failOnMaxCondition), new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) - .attr(StatChangeAttr, BattleStat.SPDEF, -1) + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.EMBER, Type.FIRE, MoveCategory.SPECIAL, 40, 100, 25, 10, 0, 1) .attr(StatusEffectAttr, StatusEffect.BURN), @@ -6584,9 +6702,9 @@ export function initMoves() { new AttackMove(Moves.PSYBEAM, Type.PSYCHIC, MoveCategory.SPECIAL, 65, 100, 20, 10, 0, 1) .attr(ConfuseAttr), new AttackMove(Moves.BUBBLE_BEAM, Type.WATER, MoveCategory.SPECIAL, 65, 100, 20, 10, 0, 1) - .attr(StatChangeAttr, BattleStat.SPD, -1), + .attr(StatStageChangeAttr, [ Stat.SPD ], -1), new AttackMove(Moves.AURORA_BEAM, Type.ICE, MoveCategory.SPECIAL, 65, 100, 20, 10, 0, 1) - .attr(StatChangeAttr, BattleStat.ATK, -1), + .attr(StatStageChangeAttr, [ Stat.ATK ], -1), new AttackMove(Moves.HYPER_BEAM, Type.NORMAL, MoveCategory.SPECIAL, 150, 90, 5, -1, 0, 1) .attr(RechargeAttr), new AttackMove(Moves.PECK, Type.FLYING, MoveCategory.PHYSICAL, 35, 100, 35, -1, 0, 1), @@ -6613,7 +6731,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.SEEDED) .condition((user, target, move) => !target.getTag(BattlerTagType.SEEDED) && !target.isOfType(Type.GRASS)), new SelfStatusMove(Moves.GROWTH, Type.NORMAL, -1, 20, -1, 0, 1) - .attr(GrowthStatChangeAttr), + .attr(GrowthStatStageChangeAttr), new AttackMove(Moves.RAZOR_LEAF, Type.GRASS, MoveCategory.PHYSICAL, 55, 95, 25, -1, 0, 1) .attr(HighCritAttr) .makesContact(false) @@ -6640,7 +6758,7 @@ export function initMoves() { .danceMove() .target(MoveTarget.RANDOM_NEAR_ENEMY), new StatusMove(Moves.STRING_SHOT, Type.BUG, 95, 40, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.SPD, -2) + .attr(StatStageChangeAttr, [ Stat.SPD ], -2) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.DRAGON_RAGE, Type.DRAGON, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 1) .attr(FixedDamageAttr, 40), @@ -6677,13 +6795,13 @@ export function initMoves() { new AttackMove(Moves.CONFUSION, Type.PSYCHIC, MoveCategory.SPECIAL, 50, 100, 25, 10, 0, 1) .attr(ConfuseAttr), new AttackMove(Moves.PSYCHIC, Type.PSYCHIC, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1) - .attr(StatChangeAttr, BattleStat.SPDEF, -1), + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1), new StatusMove(Moves.HYPNOSIS, Type.PSYCHIC, 60, 20, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.SLEEP), new SelfStatusMove(Moves.MEDITATE, Type.PSYCHIC, -1, 40, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.ATK, 1, true), + .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true), new SelfStatusMove(Moves.AGILITY, Type.PSYCHIC, -1, 30, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.SPD, 2, true), + .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true), new AttackMove(Moves.QUICK_ATTACK, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 1), new AttackMove(Moves.RAGE, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 20, -1, 0, 1) .partial(), @@ -6696,28 +6814,28 @@ export function initMoves() { .attr(MovesetCopyMoveAttr) .ignoresVirtual(), new StatusMove(Moves.SCREECH, Type.NORMAL, 85, 40, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.DEF, -2) + .attr(StatStageChangeAttr, [ Stat.DEF ], -2) .soundBased(), new SelfStatusMove(Moves.DOUBLE_TEAM, Type.NORMAL, -1, 15, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.EVA, 1, true), + .attr(StatStageChangeAttr, [ Stat.EVA ], 1, true), new SelfStatusMove(Moves.RECOVER, Type.NORMAL, -1, 5, -1, 0, 1) .attr(HealAttr, 0.5) .triageMove(), new SelfStatusMove(Moves.HARDEN, Type.NORMAL, -1, 30, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.DEF, 1, true), + .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), new SelfStatusMove(Moves.MINIMIZE, Type.NORMAL, -1, 10, -1, 0, 1) .attr(AddBattlerTagAttr, BattlerTagType.MINIMIZED, true, false) - .attr(StatChangeAttr, BattleStat.EVA, 2, true), + .attr(StatStageChangeAttr, [ Stat.EVA ], 2, true), new StatusMove(Moves.SMOKESCREEN, Type.NORMAL, 100, 20, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.ACC, -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1), new StatusMove(Moves.CONFUSE_RAY, Type.GHOST, 100, 10, -1, 0, 1) .attr(ConfuseAttr), new SelfStatusMove(Moves.WITHDRAW, Type.WATER, -1, 40, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.DEF, 1, true), + .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), new SelfStatusMove(Moves.DEFENSE_CURL, Type.NORMAL, -1, 40, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.DEF, 1, true), + .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), new SelfStatusMove(Moves.BARRIER, Type.PSYCHIC, -1, 20, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.DEF, 2, true), + .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true), new StatusMove(Moves.LIGHT_SCREEN, Type.PSYCHIC, -1, 30, -1, 0, 1) .attr(AddArenaTagAttr, ArenaTagType.LIGHT_SCREEN, 5, true) .target(MoveTarget.USER_SIDE), @@ -6765,17 +6883,17 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, -1, 0, 1) .attr(ChargeAttr, ChargeAnim.SKULL_BASH_CHARGING, i18next.t("moveTriggers:loweredItsHead", {pokemonName: "{USER}"}), null, true) - .attr(StatChangeAttr, BattleStat.DEF, 1, true) + .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true) .ignoresVirtual(), new AttackMove(Moves.SPIKE_CANNON, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 15, -1, 0, 1) .attr(MultiHitAttr) .makesContact(false), new AttackMove(Moves.CONSTRICT, Type.NORMAL, MoveCategory.PHYSICAL, 10, 100, 35, 10, 0, 1) - .attr(StatChangeAttr, BattleStat.SPD, -1), + .attr(StatStageChangeAttr, [ Stat.SPD ], -1), new SelfStatusMove(Moves.AMNESIA, Type.PSYCHIC, -1, 20, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.SPDEF, 2, true), + .attr(StatStageChangeAttr, [ Stat.SPDEF ], 2, true), new StatusMove(Moves.KINESIS, Type.PSYCHIC, 80, 15, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.ACC, -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1), new SelfStatusMove(Moves.SOFT_BOILED, Type.NORMAL, -1, 5, -1, 0, 1) .attr(HealAttr, 0.5) .triageMove(), @@ -6812,7 +6930,7 @@ export function initMoves() { .attr(TransformAttr) .ignoresProtect(), new AttackMove(Moves.BUBBLE, Type.WATER, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) - .attr(StatChangeAttr, BattleStat.SPD, -1) + .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.DIZZY_PUNCH, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 10, 20, 0, 1) .attr(ConfuseAttr) @@ -6821,13 +6939,13 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.SLEEP) .powderMove(), new StatusMove(Moves.FLASH, Type.NORMAL, 100, 20, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.ACC, -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1), new AttackMove(Moves.PSYWAVE, Type.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1) .attr(RandomLevelDamageAttr), new SelfStatusMove(Moves.SPLASH, Type.NORMAL, -1, 40, -1, 0, 1) .condition(failOnGravityCondition), new SelfStatusMove(Moves.ACID_ARMOR, Type.POISON, -1, 20, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.DEF, 2, true), + .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true), new AttackMove(Moves.CRABHAMMER, Type.WATER, MoveCategory.PHYSICAL, 100, 90, 10, -1, 0, 1) .attr(HighCritAttr), new AttackMove(Moves.EXPLOSION, Type.NORMAL, MoveCategory.PHYSICAL, 250, 100, 5, -1, 0, 1) @@ -6853,7 +6971,7 @@ export function initMoves() { .attr(FlinchAttr) .bitingMove(), new SelfStatusMove(Moves.SHARPEN, Type.NORMAL, -1, 30, -1, 0, 1) - .attr(StatChangeAttr, BattleStat.ATK, 1, true), + .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true), new SelfStatusMove(Moves.CONVERSION, Type.NORMAL, -1, 30, -1, 0, 1) .attr(FirstMoveTypeAttr), new AttackMove(Moves.TRI_ATTACK, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, 20, 0, 1) @@ -6908,7 +7026,7 @@ export function initMoves() { .windMove() .attr(HighCritAttr), new StatusMove(Moves.COTTON_SPORE, Type.GRASS, 100, 40, -1, 0, 2) - .attr(StatChangeAttr, BattleStat.SPD, -2) + .attr(StatStageChangeAttr, [ Stat.SPD ], -2) .powderMove() .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.REVERSAL, Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2) @@ -6923,21 +7041,21 @@ export function initMoves() { new AttackMove(Moves.MACH_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 2) .punchingMove(), new StatusMove(Moves.SCARY_FACE, Type.NORMAL, 100, 10, -1, 0, 2) - .attr(StatChangeAttr, BattleStat.SPD, -2), + .attr(StatStageChangeAttr, [ Stat.SPD ], -2), new AttackMove(Moves.FEINT_ATTACK, Type.DARK, MoveCategory.PHYSICAL, 60, -1, 20, -1, 0, 2), new StatusMove(Moves.SWEET_KISS, Type.FAIRY, 75, 10, -1, 0, 2) .attr(ConfuseAttr), new SelfStatusMove(Moves.BELLY_DRUM, Type.NORMAL, -1, 10, -1, 0, 2) - .attr(CutHpStatBoostAttr, [BattleStat.ATK], 12, 2, (user) => { - user.scene.queueMessage(i18next.t("moveTriggers:cutOwnHpAndMaximizedStat", {pokemonName: getPokemonNameWithAffix(user), statName: getBattleStatName(BattleStat.ATK)})); + .attr(CutHpStatStageBoostAttr, [ Stat.ATK ], 12, 2, (user) => { + user.scene.queueMessage(i18next.t("moveTriggers:cutOwnHpAndMaximizedStat", { pokemonName: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) })); }), new AttackMove(Moves.SLUDGE_BOMB, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 30, 0, 2) .attr(StatusEffectAttr, StatusEffect.POISON) .ballBombMove(), new AttackMove(Moves.MUD_SLAP, Type.GROUND, MoveCategory.SPECIAL, 20, 100, 10, 100, 0, 2) - .attr(StatChangeAttr, BattleStat.ACC, -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1), new AttackMove(Moves.OCTAZOOKA, Type.WATER, MoveCategory.SPECIAL, 65, 85, 10, 50, 0, 2) - .attr(StatChangeAttr, BattleStat.ACC, -1) + .attr(StatStageChangeAttr, [ Stat.ACC ], -1) .ballBombMove(), new StatusMove(Moves.SPIKES, Type.GROUND, -1, 20, -1, 0, 2) .attr(AddArenaTrapTagAttr, ArenaTagType.SPIKES) @@ -6966,7 +7084,7 @@ export function initMoves() { .condition(failOnBossCondition) .target(MoveTarget.ALL), new AttackMove(Moves.ICY_WIND, Type.ICE, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 2) - .attr(StatChangeAttr, BattleStat.SPD, -1) + .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .windMove() .target(MoveTarget.ALL_NEAR_ENEMIES), new SelfStatusMove(Moves.DETECT, Type.FIGHTING, -1, 5, -1, 4, 2) @@ -6990,13 +7108,13 @@ export function initMoves() { new SelfStatusMove(Moves.ENDURE, Type.NORMAL, -1, 10, -1, 4, 2) .attr(ProtectAttr, BattlerTagType.ENDURING), new StatusMove(Moves.CHARM, Type.FAIRY, 100, 20, -1, 0, 2) - .attr(StatChangeAttr, BattleStat.ATK, -2), + .attr(StatStageChangeAttr, [ Stat.ATK ], -2), new AttackMove(Moves.ROLLOUT, Type.ROCK, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 2) .attr(ConsecutiveUseDoublePowerAttr, 5, true, true, Moves.DEFENSE_CURL), new AttackMove(Moves.FALSE_SWIPE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 2) .attr(SurviveDamageAttr), new StatusMove(Moves.SWAGGER, Type.NORMAL, 85, 15, -1, 0, 2) - .attr(StatChangeAttr, BattleStat.ATK, 2) + .attr(StatStageChangeAttr, [ Stat.ATK ], 2) .attr(ConfuseAttr), new SelfStatusMove(Moves.MILK_DRINK, Type.NORMAL, -1, 5, -1, 0, 2) .attr(HealAttr, 0.5) @@ -7007,7 +7125,7 @@ export function initMoves() { .attr(ConsecutiveUseDoublePowerAttr, 3, true) .slicingMove(), new AttackMove(Moves.STEEL_WING, Type.STEEL, MoveCategory.PHYSICAL, 70, 90, 25, 10, 0, 2) - .attr(StatChangeAttr, BattleStat.DEF, 1, true), + .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), new StatusMove(Moves.MEAN_LOOK, Type.NORMAL, -1, 5, -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2) @@ -7061,7 +7179,7 @@ export function initMoves() { new AttackMove(Moves.PURSUIT, Type.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2) .partial(), new AttackMove(Moves.RAPID_SPIN, Type.NORMAL, MoveCategory.PHYSICAL, 50, 100, 40, 100, 0, 2) - .attr(StatChangeAttr, BattleStat.SPD, 1, true) + .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true) .attr(RemoveBattlerTagAttr, [ BattlerTagType.BIND, BattlerTagType.WRAP, @@ -7077,12 +7195,12 @@ export function initMoves() { ], true) .attr(RemoveArenaTrapAttr), new StatusMove(Moves.SWEET_SCENT, Type.NORMAL, 100, 20, -1, 0, 2) - .attr(StatChangeAttr, BattleStat.EVA, -2) + .attr(StatStageChangeAttr, [ Stat.EVA ], -2) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.IRON_TAIL, Type.STEEL, MoveCategory.PHYSICAL, 100, 75, 15, 30, 0, 2) - .attr(StatChangeAttr, BattleStat.DEF, -1), + .attr(StatStageChangeAttr, [ Stat.DEF ], -1), new AttackMove(Moves.METAL_CLAW, Type.STEEL, MoveCategory.PHYSICAL, 50, 95, 35, 10, 0, 2) - .attr(StatChangeAttr, BattleStat.ATK, 1, true), + .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true), new AttackMove(Moves.VITAL_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 70, -1, 10, -1, -1, 2), new SelfStatusMove(Moves.MORNING_SUN, Type.NORMAL, -1, 5, -1, 0, 2) .attr(PlantHealAttr) @@ -7109,7 +7227,7 @@ export function initMoves() { .attr(WeatherChangeAttr, WeatherType.SUNNY) .target(MoveTarget.BOTH_SIDES), new AttackMove(Moves.CRUNCH, Type.DARK, MoveCategory.PHYSICAL, 80, 100, 15, 20, 0, 2) - .attr(StatChangeAttr, BattleStat.DEF, -1) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1) .bitingMove(), new AttackMove(Moves.MIRROR_COAT, Type.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 20, -1, -5, 2) .attr(CounterDamageAttr, (move: Move) => move.category === MoveCategory.SPECIAL, 2) @@ -7118,15 +7236,15 @@ export function initMoves() { .attr(CopyStatsAttr), new AttackMove(Moves.EXTREME_SPEED, Type.NORMAL, MoveCategory.PHYSICAL, 80, 100, 5, -1, 2, 2), new AttackMove(Moves.ANCIENT_POWER, Type.ROCK, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 2) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true), new AttackMove(Moves.SHADOW_BALL, Type.GHOST, MoveCategory.SPECIAL, 80, 100, 15, 20, 0, 2) - .attr(StatChangeAttr, BattleStat.SPDEF, -1) + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .ballBombMove(), new AttackMove(Moves.FUTURE_SIGHT, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 2) .partial() .attr(DelayedAttackAttr, ArenaTagType.FUTURE_SIGHT, ChargeAnim.FUTURE_SIGHT_CHARGING, i18next.t("moveTriggers:foresawAnAttack", {pokemonName: "{USER}"})), new AttackMove(Moves.ROCK_SMASH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 15, 50, 0, 2) - .attr(StatChangeAttr, BattleStat.DEF, -1), + .attr(StatStageChangeAttr, [ Stat.DEF ], -1), new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2) .attr(TrapAttr, BattlerTagType.WHIRLPOOL) .attr(HitsTagAttr, BattlerTagType.UNDERWATER, true), @@ -7165,13 +7283,13 @@ export function initMoves() { new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3) .unimplemented(), new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3) - .attr(StatChangeAttr, BattleStat.SPATK, 1) + .attr(StatStageChangeAttr, [ Stat.SPATK ], 1) .attr(ConfuseAttr), new StatusMove(Moves.WILL_O_WISP, Type.FIRE, 85, 15, -1, 0, 3) .attr(StatusEffectAttr, StatusEffect.BURN), new StatusMove(Moves.MEMENTO, Type.DARK, 100, 10, -1, 0, 3) .attr(SacrificialAttrOnHit) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], -2), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -2), new AttackMove(Moves.FACADE, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 3) .attr(MovePowerMultiplierAttr, (user, target, move) => user.status && (user.status.effect === StatusEffect.BURN || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.PARALYSIS) ? 2 : 1) @@ -7190,7 +7308,7 @@ export function initMoves() { .attr(NaturePowerAttr) .ignoresVirtual(), new SelfStatusMove(Moves.CHARGE, Type.ELECTRIC, -1, 20, -1, 0, 3) - .attr(StatChangeAttr, BattleStat.SPDEF, 1, true) + .attr(StatStageChangeAttr, [ Stat.SPDEF ], 1, true) .attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false), new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3) .unimplemented(), @@ -7210,7 +7328,7 @@ export function initMoves() { 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, -1, 0, 3) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF ], -1, true), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1, true), new SelfStatusMove(Moves.MAGIC_COAT, Type.PSYCHIC, -1, 15, -1, 4, 3) .unimplemented(), new SelfStatusMove(Moves.RECYCLE, Type.NORMAL, -1, 10, -1, 0, 3) @@ -7254,14 +7372,14 @@ export function initMoves() { new SelfStatusMove(Moves.CAMOUFLAGE, Type.NORMAL, -1, 20, -1, 0, 3) .attr(CopyBiomeTypeAttr), new SelfStatusMove(Moves.TAIL_GLOW, Type.BUG, -1, 20, -1, 0, 3) - .attr(StatChangeAttr, BattleStat.SPATK, 3, true), + .attr(StatStageChangeAttr, [ Stat.SPATK ], 3, true), new AttackMove(Moves.LUSTER_PURGE, Type.PSYCHIC, MoveCategory.SPECIAL, 95, 100, 5, 50, 0, 3) - .attr(StatChangeAttr, BattleStat.SPDEF, -1), + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1), new AttackMove(Moves.MIST_BALL, Type.PSYCHIC, MoveCategory.SPECIAL, 95, 100, 5, 50, 0, 3) - .attr(StatChangeAttr, BattleStat.SPATK, -1) + .attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .ballBombMove(), new StatusMove(Moves.FEATHER_DANCE, Type.FLYING, 100, 15, -1, 0, 3) - .attr(StatChangeAttr, BattleStat.ATK, -2) + .attr(StatStageChangeAttr, [ Stat.ATK ], -2) .danceMove(), new StatusMove(Moves.TEETER_DANCE, Type.NORMAL, 100, 20, -1, 0, 3) .attr(ConfuseAttr) @@ -7288,13 +7406,13 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.TOXIC) .bitingMove(), new AttackMove(Moves.CRUSH_CLAW, Type.NORMAL, MoveCategory.PHYSICAL, 75, 95, 10, 50, 0, 3) - .attr(StatChangeAttr, BattleStat.DEF, -1), + .attr(StatStageChangeAttr, [ Stat.DEF ], -1), new AttackMove(Moves.BLAST_BURN, Type.FIRE, MoveCategory.SPECIAL, 150, 90, 5, -1, 0, 3) .attr(RechargeAttr), new AttackMove(Moves.HYDRO_CANNON, Type.WATER, MoveCategory.SPECIAL, 150, 90, 5, -1, 0, 3) .attr(RechargeAttr), new AttackMove(Moves.METEOR_MASH, Type.STEEL, MoveCategory.PHYSICAL, 90, 90, 10, 20, 0, 3) - .attr(StatChangeAttr, BattleStat.ATK, 1, true) + .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true) .punchingMove(), new AttackMove(Moves.ASTONISH, Type.GHOST, MoveCategory.PHYSICAL, 30, 100, 15, 30, 0, 3) .attr(FlinchAttr), @@ -7306,33 +7424,33 @@ export function initMoves() { .attr(PartyStatusCureAttr, i18next.t("moveTriggers:soothingAromaWaftedThroughArea"), Abilities.SAP_SIPPER) .target(MoveTarget.PARTY), new StatusMove(Moves.FAKE_TEARS, Type.DARK, 100, 20, -1, 0, 3) - .attr(StatChangeAttr, BattleStat.SPDEF, -2), + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), new AttackMove(Moves.AIR_CUTTER, Type.FLYING, MoveCategory.SPECIAL, 60, 95, 25, -1, 0, 3) .attr(HighCritAttr) .slicingMove() .windMove() .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.OVERHEAT, Type.FIRE, MoveCategory.SPECIAL, 130, 90, 5, -1, 0, 3) - .attr(StatChangeAttr, BattleStat.SPATK, -2, true) + .attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true) .attr(HealStatusEffectAttr, true, StatusEffect.FREEZE), new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3) .attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST), new AttackMove(Moves.ROCK_TOMB, Type.ROCK, MoveCategory.PHYSICAL, 60, 95, 15, 100, 0, 3) - .attr(StatChangeAttr, BattleStat.SPD, -1) + .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .makesContact(false), new AttackMove(Moves.SILVER_WIND, Type.BUG, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 3) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .windMove(), new StatusMove(Moves.METAL_SOUND, Type.STEEL, 85, 40, -1, 0, 3) - .attr(StatChangeAttr, BattleStat.SPDEF, -2) + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2) .soundBased(), new StatusMove(Moves.GRASS_WHISTLE, Type.GRASS, 55, 15, -1, 0, 3) .attr(StatusEffectAttr, StatusEffect.SLEEP) .soundBased(), new StatusMove(Moves.TICKLE, Type.NORMAL, 100, 20, -1, 0, 3) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF ], -1), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1), new SelfStatusMove(Moves.COSMIC_POWER, Type.PSYCHIC, -1, 20, -1, 0, 3) - .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], 1, true), + .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, true), new AttackMove(Moves.WATER_SPOUT, Type.WATER, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 3) .attr(HpPowerAttr) .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -7353,7 +7471,7 @@ export function initMoves() { .attr(OneHitKOAttr) .attr(SheerColdAccuracyAttr), new AttackMove(Moves.MUDDY_WATER, Type.WATER, MoveCategory.SPECIAL, 90, 85, 10, 30, 0, 3) - .attr(StatChangeAttr, BattleStat.ACC, -1) + .attr(StatStageChangeAttr, [ Stat.ACC ], -1) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.BULLET_SEED, Type.GRASS, MoveCategory.PHYSICAL, 25, 100, 30, -1, 0, 3) .attr(MultiHitAttr) @@ -7365,25 +7483,25 @@ export function initMoves() { .attr(MultiHitAttr) .makesContact(false), new SelfStatusMove(Moves.IRON_DEFENSE, Type.STEEL, -1, 15, -1, 0, 3) - .attr(StatChangeAttr, BattleStat.DEF, 2, true), + .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true), new StatusMove(Moves.BLOCK, Type.NORMAL, -1, 5, -1, 0, 3) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), new StatusMove(Moves.HOWL, Type.NORMAL, -1, 40, -1, 0, 3) - .attr(StatChangeAttr, BattleStat.ATK, 1) + .attr(StatStageChangeAttr, [ Stat.ATK ], 1) .soundBased() .target(MoveTarget.USER_AND_ALLIES), new AttackMove(Moves.DRAGON_CLAW, Type.DRAGON, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 3), new AttackMove(Moves.FRENZY_PLANT, Type.GRASS, MoveCategory.SPECIAL, 150, 90, 5, -1, 0, 3) .attr(RechargeAttr), new SelfStatusMove(Moves.BULK_UP, Type.FIGHTING, -1, 20, -1, 0, 3) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF ], 1, true), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1, true), new AttackMove(Moves.BOUNCE, Type.FLYING, MoveCategory.PHYSICAL, 85, 85, 5, 30, 0, 3) .attr(ChargeAttr, ChargeAnim.BOUNCE_CHARGING, i18next.t("moveTriggers:sprangUp", {pokemonName: "{USER}"}), BattlerTagType.FLYING) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .condition(failOnGravityCondition) .ignoresVirtual(), new AttackMove(Moves.MUD_SHOT, Type.GROUND, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 3) - .attr(StatChangeAttr, BattleStat.SPD, -1), + .attr(StatStageChangeAttr, [ Stat.SPD ], -1), new AttackMove(Moves.POISON_TAIL, Type.POISON, MoveCategory.PHYSICAL, 50, 100, 25, 10, 0, 3) .attr(HighCritAttr) .attr(StatusEffectAttr, StatusEffect.POISON), @@ -7398,12 +7516,12 @@ export function initMoves() { .attr(AddArenaTagAttr, ArenaTagType.WATER_SPORT, 5) .target(MoveTarget.BOTH_SIDES), new SelfStatusMove(Moves.CALM_MIND, Type.PSYCHIC, -1, 20, -1, 0, 3) - .attr(StatChangeAttr, [ BattleStat.SPATK, BattleStat.SPDEF ], 1, true), + .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF ], 1, true), new AttackMove(Moves.LEAF_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 3) .attr(HighCritAttr) .slicingMove(), new SelfStatusMove(Moves.DRAGON_DANCE, Type.DRAGON, -1, 20, -1, 0, 3) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPD ], 1, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true) .danceMove(), new AttackMove(Moves.ROCK_BLAST, Type.ROCK, MoveCategory.PHYSICAL, 25, 90, 10, -1, 0, 3) .attr(MultiHitAttr) @@ -7417,7 +7535,7 @@ export function initMoves() { .partial() .attr(DelayedAttackAttr, ArenaTagType.DOOM_DESIRE, ChargeAnim.DOOM_DESIRE_CHARGING, i18next.t("moveTriggers:choseDoomDesireAsDestiny", {pokemonName: "{USER}"})), new AttackMove(Moves.PSYCHO_BOOST, Type.PSYCHIC, MoveCategory.SPECIAL, 140, 90, 5, -1, 0, 3) - .attr(StatChangeAttr, BattleStat.SPATK, -2, true), + .attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true), new SelfStatusMove(Moves.ROOST, Type.FLYING, -1, 5, -1, 0, 4) .attr(HealAttr, 0.5) .attr(AddBattlerTagAttr, BattlerTagType.ROOSTED, true, false) @@ -7431,7 +7549,7 @@ export function initMoves() { .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, -1, 0, 4) - .attr(StatChangeAttr, BattleStat.SPD, -1, true) + .attr(StatStageChangeAttr, [ Stat.SPD ], -1, true) .punchingMove(), new AttackMove(Moves.GYRO_BALL, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 4) .attr(GyroBallPowerAttr) @@ -7456,7 +7574,7 @@ export function initMoves() { .attr(AddArenaTagAttr, ArenaTagType.TAILWIND, 4, true) .target(MoveTarget.USER_SIDE), new StatusMove(Moves.ACUPRESSURE, Type.NORMAL, -1, 30, -1, 0, 4) - .attr(AcupressureStatChangeAttr) + .attr(AcupressureStatStageChangeAttr) .target(MoveTarget.USER_OR_NEAR_ALLY), new AttackMove(Moves.METAL_BURST, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 4) .attr(CounterDamageAttr, (move: Move) => (move.category === MoveCategory.PHYSICAL || move.category === MoveCategory.SPECIAL), 1.5) @@ -7466,7 +7584,7 @@ export function initMoves() { 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, -1, 0, 4) - .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], -1, true), + .attr(StatStageChangeAttr, [ Stat.DEF, Stat.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), new AttackMove(Moves.ASSURANCE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 4) @@ -7510,9 +7628,9 @@ export function initMoves() { .attr(CopyMoveAttr) .ignoresVirtual(), new StatusMove(Moves.POWER_SWAP, Type.PSYCHIC, -1, 10, 100, 0, 4) - .unimplemented(), + .attr(SwapStatStagesAttr, [ Stat.ATK, Stat.SPATK ]), new StatusMove(Moves.GUARD_SWAP, Type.PSYCHIC, -1, 10, 100, 0, 4) - .unimplemented(), + .attr(SwapStatStagesAttr, [ Stat.DEF, Stat.SPDEF ]), new AttackMove(Moves.PUNISHMENT, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 4) .makesContact(true) .attr(PunishmentPowerAttr), @@ -7526,7 +7644,7 @@ export function initMoves() { .attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES) .target(MoveTarget.ENEMY_SIDE), new StatusMove(Moves.HEART_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 4) - .attr(SwapStatsAttr), + .attr(SwapStatStagesAttr, BATTLE_STATS), new SelfStatusMove(Moves.AQUA_RING, Type.WATER, -1, 20, -1, 0, 4) .attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true), new SelfStatusMove(Moves.MAGNET_RISE, Type.ELECTRIC, -1, 10, -1, 0, 4) @@ -7543,7 +7661,7 @@ export function initMoves() { .pulseMove() .ballBombMove(), new SelfStatusMove(Moves.ROCK_POLISH, Type.ROCK, -1, 20, -1, 0, 4) - .attr(StatChangeAttr, BattleStat.SPD, 2, true), + .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true), new AttackMove(Moves.POISON_JAB, Type.POISON, MoveCategory.PHYSICAL, 80, 100, 20, 30, 0, 4) .attr(StatusEffectAttr, StatusEffect.POISON), new AttackMove(Moves.DARK_PULSE, Type.DARK, MoveCategory.SPECIAL, 80, 100, 15, 20, 0, 4) @@ -7562,7 +7680,7 @@ export function initMoves() { new AttackMove(Moves.X_SCISSOR, Type.BUG, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 4) .slicingMove(), new AttackMove(Moves.BUG_BUZZ, Type.BUG, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 4) - .attr(StatChangeAttr, BattleStat.SPDEF, -1) + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .soundBased(), new AttackMove(Moves.DRAGON_PULSE, Type.DRAGON, MoveCategory.SPECIAL, 85, 100, 10, -1, 0, 4) .pulseMove(), @@ -7577,22 +7695,22 @@ export function initMoves() { .triageMove(), new AttackMove(Moves.VACUUM_WAVE, Type.FIGHTING, MoveCategory.SPECIAL, 40, 100, 30, -1, 1, 4), new AttackMove(Moves.FOCUS_BLAST, Type.FIGHTING, MoveCategory.SPECIAL, 120, 70, 5, 10, 0, 4) - .attr(StatChangeAttr, BattleStat.SPDEF, -1) + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .ballBombMove(), new AttackMove(Moves.ENERGY_BALL, Type.GRASS, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 4) - .attr(StatChangeAttr, BattleStat.SPDEF, -1) + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .ballBombMove(), new AttackMove(Moves.BRAVE_BIRD, Type.FLYING, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 4) .attr(RecoilAttr, false, 0.33) .recklessMove(), new AttackMove(Moves.EARTH_POWER, Type.GROUND, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 4) - .attr(StatChangeAttr, BattleStat.SPDEF, -1), + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1), new StatusMove(Moves.SWITCHEROO, Type.DARK, 100, 10, -1, 0, 4) .unimplemented(), new AttackMove(Moves.GIGA_IMPACT, Type.NORMAL, MoveCategory.PHYSICAL, 150, 90, 5, -1, 0, 4) .attr(RechargeAttr), new SelfStatusMove(Moves.NASTY_PLOT, Type.DARK, -1, 20, -1, 0, 4) - .attr(StatChangeAttr, BattleStat.SPATK, 2, true), + .attr(StatStageChangeAttr, [ Stat.SPATK ], 2, true), new AttackMove(Moves.BULLET_PUNCH, Type.STEEL, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 4) .punchingMove(), new AttackMove(Moves.AVALANCHE, Type.ICE, MoveCategory.PHYSICAL, 60, 100, 10, -1, -4, 4) @@ -7615,7 +7733,7 @@ export function initMoves() { .bitingMove(), new AttackMove(Moves.SHADOW_SNEAK, Type.GHOST, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 4), new AttackMove(Moves.MUD_BOMB, Type.GROUND, MoveCategory.SPECIAL, 65, 85, 10, 30, 0, 4) - .attr(StatChangeAttr, BattleStat.ACC, -1) + .attr(StatStageChangeAttr, [ Stat.ACC ], -1) .ballBombMove(), new AttackMove(Moves.PSYCHO_CUT, Type.PSYCHIC, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 4) .attr(HighCritAttr) @@ -7624,13 +7742,13 @@ export function initMoves() { new AttackMove(Moves.ZEN_HEADBUTT, Type.PSYCHIC, MoveCategory.PHYSICAL, 80, 90, 15, 20, 0, 4) .attr(FlinchAttr), new AttackMove(Moves.MIRROR_SHOT, Type.STEEL, MoveCategory.SPECIAL, 65, 85, 10, 30, 0, 4) - .attr(StatChangeAttr, BattleStat.ACC, -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1), new AttackMove(Moves.FLASH_CANNON, Type.STEEL, MoveCategory.SPECIAL, 80, 100, 10, 10, 0, 4) - .attr(StatChangeAttr, BattleStat.SPDEF, -1), + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1), new AttackMove(Moves.ROCK_CLIMB, Type.NORMAL, MoveCategory.PHYSICAL, 90, 85, 20, 20, 0, 4) .attr(ConfuseAttr), new StatusMove(Moves.DEFOG, Type.FLYING, -1, 15, -1, 0, 4) - .attr(StatChangeAttr, BattleStat.EVA, -1) + .attr(StatStageChangeAttr, [ Stat.EVA ], -1) .attr(ClearWeatherAttr, WeatherType.FOG) .attr(ClearTerrainAttr) .attr(RemoveScreensAttr, false) @@ -7640,7 +7758,7 @@ export function initMoves() { .ignoresProtect() .target(MoveTarget.BOTH_SIDES), new AttackMove(Moves.DRACO_METEOR, Type.DRAGON, MoveCategory.SPECIAL, 130, 90, 5, -1, 0, 4) - .attr(StatChangeAttr, BattleStat.SPATK, -2, true), + .attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true), new AttackMove(Moves.DISCHARGE, Type.ELECTRIC, MoveCategory.SPECIAL, 80, 100, 15, 30, 0, 4) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .target(MoveTarget.ALL_NEAR_OTHERS), @@ -7648,7 +7766,7 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.BURN) .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.LEAF_STORM, Type.GRASS, MoveCategory.SPECIAL, 130, 90, 5, -1, 0, 4) - .attr(StatChangeAttr, BattleStat.SPATK, -2, true), + .attr(StatStageChangeAttr, [ Stat.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) .attr(RechargeAttr) @@ -7670,7 +7788,7 @@ export function initMoves() { .attr(HighCritAttr) .makesContact(false), new StatusMove(Moves.CAPTIVATE, Type.NORMAL, 100, 20, -1, 0, 4) - .attr(StatChangeAttr, BattleStat.SPATK, -2) + .attr(StatStageChangeAttr, [ Stat.SPATK ], -2) .condition((user, target, move) => target.isOppositeGender(user)) .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.STEALTH_ROCK, Type.ROCK, -1, 20, -1, 0, 4) @@ -7688,7 +7806,7 @@ export function initMoves() { new AttackMove(Moves.BUG_BITE, Type.BUG, MoveCategory.PHYSICAL, 60, 100, 20, -1, 0, 4) .attr(StealEatBerryAttr), new AttackMove(Moves.CHARGE_BEAM, Type.ELECTRIC, MoveCategory.SPECIAL, 50, 90, 10, 70, 0, 4) - .attr(StatChangeAttr, BattleStat.SPATK, 1, true), + .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true), new AttackMove(Moves.WOOD_HAMMER, Type.GRASS, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 4) .attr(RecoilAttr, false, 0.33) .recklessMove(), @@ -7697,7 +7815,7 @@ export function initMoves() { .attr(HighCritAttr) .makesContact(false), new SelfStatusMove(Moves.DEFEND_ORDER, Type.BUG, -1, 10, -1, 0, 4) - .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], 1, true), + .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, true), new SelfStatusMove(Moves.HEAL_ORDER, Type.BUG, -1, 10, -1, 0, 4) .attr(HealAttr, 0.5) .triageMove(), @@ -7723,23 +7841,23 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.SLEEP) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.SEED_FLARE, Type.GRASS, MoveCategory.SPECIAL, 120, 85, 5, 40, 0, 4) - .attr(StatChangeAttr, BattleStat.SPDEF, -2), + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), new AttackMove(Moves.OMINOUS_WIND, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 4) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .windMove(), new AttackMove(Moves.SHADOW_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4) .attr(ChargeAttr, ChargeAnim.SHADOW_FORCE_CHARGING, i18next.t("moveTriggers:vanishedInstantly", {pokemonName: "{USER}"}), BattlerTagType.HIDDEN) .ignoresProtect() .ignoresVirtual(), new SelfStatusMove(Moves.HONE_CLAWS, Type.DARK, -1, 15, -1, 0, 5) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.ACC ], 1, true), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.ACC ], 1, true), new StatusMove(Moves.WIDE_GUARD, Type.ROCK, -1, 10, -1, 3, 5) .target(MoveTarget.USER_SIDE) .attr(AddArenaTagAttr, ArenaTagType.WIDE_GUARD, 1, true, true), new StatusMove(Moves.GUARD_SPLIT, Type.PSYCHIC, -1, 10, -1, 0, 5) - .unimplemented(), + .attr(AverageStatsAttr, [ Stat.DEF, Stat.SPDEF ], "moveTriggers:sharedGuard"), new StatusMove(Moves.POWER_SPLIT, Type.PSYCHIC, -1, 10, -1, 0, 5) - .unimplemented(), + .attr(AverageStatsAttr, [ Stat.ATK, Stat.SPATK ], "moveTriggers:sharedPower"), new StatusMove(Moves.WONDER_ROOM, Type.PSYCHIC, -1, 10, -1, 0, 5) .ignoresProtect() .target(MoveTarget.BOTH_SIDES) @@ -7749,7 +7867,7 @@ export function initMoves() { new AttackMove(Moves.VENOSHOCK, Type.POISON, MoveCategory.SPECIAL, 65, 100, 10, -1, 0, 5) .attr(MovePowerMultiplierAttr, (user, target, move) => target.status && (target.status.effect === StatusEffect.POISON || target.status.effect === StatusEffect.TOXIC) ? 2 : 1), new SelfStatusMove(Moves.AUTOTOMIZE, Type.STEEL, -1, 15, -1, 0, 5) - .attr(StatChangeAttr, BattleStat.SPD, 2, true) + .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true) .partial(), new SelfStatusMove(Moves.RAGE_POWDER, Type.BUG, -1, 20, -1, 2, 5) .powderMove() @@ -7775,7 +7893,7 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.POISON) .target(MoveTarget.ALL_NEAR_OTHERS), new SelfStatusMove(Moves.QUIVER_DANCE, Type.BUG, -1, 20, -1, 0, 5) - .attr(StatChangeAttr, [ BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true) + .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .danceMove(), new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) .attr(MinimizeAccuracyAttr) @@ -7792,13 +7910,13 @@ export function initMoves() { new StatusMove(Moves.SOAK, Type.WATER, 100, 20, -1, 0, 5) .attr(ChangeTypeAttr, Type.WATER), new AttackMove(Moves.FLAME_CHARGE, Type.FIRE, MoveCategory.PHYSICAL, 50, 100, 20, 100, 0, 5) - .attr(StatChangeAttr, BattleStat.SPD, 1, true), + .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true), new SelfStatusMove(Moves.COIL, Type.POISON, -1, 20, -1, 0, 5) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.ACC ], 1, true), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.ACC ], 1, true), new AttackMove(Moves.LOW_SWEEP, Type.FIGHTING, MoveCategory.PHYSICAL, 65, 100, 20, 100, 0, 5) - .attr(StatChangeAttr, BattleStat.SPD, -1), + .attr(StatStageChangeAttr, [ Stat.SPD ], -1), new AttackMove(Moves.ACID_SPRAY, Type.POISON, MoveCategory.SPECIAL, 40, 100, 20, 100, 0, 5) - .attr(StatChangeAttr, BattleStat.SPDEF, -2) + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2) .ballBombMove(), new AttackMove(Moves.FOUL_PLAY, Type.DARK, MoveCategory.PHYSICAL, 95, 100, 15, -1, 0, 5) .attr(TargetAtkUserAtkAttr), @@ -7816,11 +7934,11 @@ export function initMoves() { .attr(ConsecutiveUseMultiBasePowerAttr, 5, false) .soundBased(), new AttackMove(Moves.CHIP_AWAY, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 5) - .attr(IgnoreOpponentStatChangesAttr), + .attr(IgnoreOpponentStatStagesAttr), new AttackMove(Moves.CLEAR_SMOG, Type.POISON, MoveCategory.SPECIAL, 50, -1, 15, -1, 0, 5) .attr(ResetStatsAttr, false), new AttackMove(Moves.STORED_POWER, Type.PSYCHIC, MoveCategory.SPECIAL, 20, 100, 10, -1, 0, 5) - .attr(StatChangeCountPowerAttr), + .attr(PositiveStatStagePowerAttr), new StatusMove(Moves.QUICK_GUARD, Type.FIGHTING, -1, 15, -1, 3, 5) .target(MoveTarget.USER_SIDE) .attr(AddArenaTagAttr, ArenaTagType.QUICK_GUARD, 1, true, true), @@ -7832,8 +7950,8 @@ export function initMoves() { .attr(HealStatusEffectAttr, true, StatusEffect.FREEZE) .attr(StatusEffectAttr, StatusEffect.BURN), new SelfStatusMove(Moves.SHELL_SMASH, Type.NORMAL, -1, 15, -1, 0, 5) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK, BattleStat.SPD ], 2, true) - .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], -1, true), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], 2, true) + .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true), new StatusMove(Moves.HEAL_PULSE, Type.PSYCHIC, -1, 10, -1, 0, 5) .attr(HealAttr, 0.5, false, false) .pulseMove() @@ -7847,8 +7965,8 @@ export function initMoves() { .condition(failOnGravityCondition) .ignoresVirtual(), new SelfStatusMove(Moves.SHIFT_GEAR, Type.STEEL, -1, 10, -1, 0, 5) - .attr(StatChangeAttr, BattleStat.ATK, 1, true) - .attr(StatChangeAttr, BattleStat.SPD, 2, true), + .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true) + .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true), new AttackMove(Moves.CIRCLE_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 90, 10, -1, -6, 5) .attr(ForceSwitchOutAttr), new AttackMove(Moves.INCINERATE, Type.FIRE, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5) @@ -7879,10 +7997,10 @@ export function initMoves() { new AttackMove(Moves.VOLT_SWITCH, Type.ELECTRIC, MoveCategory.SPECIAL, 70, 100, 20, -1, 0, 5) .attr(ForceSwitchOutAttr, true, false), new AttackMove(Moves.STRUGGLE_BUG, Type.BUG, MoveCategory.SPECIAL, 50, 100, 20, 100, 0, 5) - .attr(StatChangeAttr, BattleStat.SPATK, -1) + .attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.BULLDOZE, Type.GROUND, MoveCategory.PHYSICAL, 60, 100, 20, 100, 0, 5) - .attr(StatChangeAttr, BattleStat.SPD, -1) + .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .makesContact(false) .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FROST_BREATH, Type.ICE, MoveCategory.SPECIAL, 60, 90, 10, 100, 0, 5) @@ -7891,9 +8009,9 @@ export function initMoves() { .attr(ForceSwitchOutAttr) .hidesTarget(), new SelfStatusMove(Moves.WORK_UP, Type.NORMAL, -1, 30, -1, 0, 5) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], 1, true), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, true), new AttackMove(Moves.ELECTROWEB, Type.ELECTRIC, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5) - .attr(StatChangeAttr, BattleStat.SPD, -1) + .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.WILD_CHARGE, Type.ELECTRIC, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 5) .attr(RecoilAttr) @@ -7908,10 +8026,10 @@ export function initMoves() { .attr(HitHealAttr) .triageMove(), new AttackMove(Moves.SACRED_SWORD, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 5) - .attr(IgnoreOpponentStatChangesAttr) + .attr(IgnoreOpponentStatStagesAttr) .slicingMove(), new AttackMove(Moves.RAZOR_SHELL, Type.WATER, MoveCategory.PHYSICAL, 75, 95, 10, 50, 0, 5) - .attr(StatChangeAttr, BattleStat.DEF, -1) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1) .slicingMove(), new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) .attr(MinimizeAccuracyAttr) @@ -7919,13 +8037,13 @@ export function initMoves() { .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) .condition(failOnMaxCondition), new AttackMove(Moves.LEAF_TORNADO, Type.GRASS, MoveCategory.SPECIAL, 65, 90, 10, 50, 0, 5) - .attr(StatChangeAttr, BattleStat.ACC, -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1), new AttackMove(Moves.STEAMROLLER, Type.BUG, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 5) .attr(FlinchAttr), new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5) - .attr(StatChangeAttr, BattleStat.DEF, 3, true), + .attr(StatStageChangeAttr, [ Stat.DEF ], 3, true), new AttackMove(Moves.NIGHT_DAZE, Type.DARK, MoveCategory.SPECIAL, 85, 95, 10, 40, 0, 5) - .attr(StatChangeAttr, BattleStat.ACC, -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1), new AttackMove(Moves.PSYSTRIKE, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 10, -1, 0, 5) .attr(DefDefAttr), new AttackMove(Moves.TAIL_SLAP, Type.NORMAL, MoveCategory.PHYSICAL, 25, 85, 10, -1, 0, 5) @@ -7954,14 +8072,14 @@ export function initMoves() { .attr(DefDefAttr) .slicingMove(), new AttackMove(Moves.GLACIATE, Type.ICE, MoveCategory.SPECIAL, 65, 95, 10, 100, 0, 5) - .attr(StatChangeAttr, BattleStat.SPD, -1) + .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.BOLT_STRIKE, Type.ELECTRIC, MoveCategory.PHYSICAL, 130, 85, 5, 20, 0, 5) .attr(StatusEffectAttr, StatusEffect.PARALYSIS), new AttackMove(Moves.BLUE_FLARE, Type.FIRE, MoveCategory.SPECIAL, 130, 85, 5, 20, 0, 5) .attr(StatusEffectAttr, StatusEffect.BURN), new AttackMove(Moves.FIERY_DANCE, Type.FIRE, MoveCategory.SPECIAL, 80, 100, 10, 50, 0, 5) - .attr(StatChangeAttr, BattleStat.SPATK, 1, true) + .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) .danceMove(), new AttackMove(Moves.FREEZE_SHOCK, Type.ICE, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 5) .attr(ChargeAttr, ChargeAnim.FREEZE_SHOCK_CHARGING, i18next.t("moveTriggers:becameCloakedInFreezingLight", {pokemonName: "{USER}"})) @@ -7972,14 +8090,14 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.BURN) .ignoresVirtual(), new AttackMove(Moves.SNARL, Type.DARK, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5) - .attr(StatChangeAttr, BattleStat.SPATK, -1) + .attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), 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, -1, 0, 5) - .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF, BattleStat.SPD ], -1, true), + .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF, Stat.SPD ], -1, true), new AttackMove(Moves.FUSION_FLARE, Type.FIRE, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 5) .attr(HealStatusEffectAttr, true, StatusEffect.FREEZE) .attr(LastMoveDoublePowerAttr, Moves.FUSION_BOLT), @@ -8003,12 +8121,12 @@ export function initMoves() { // If any fielded pokémon is grass-type and grounded. return [...user.scene.getEnemyParty(), ...user.scene.getParty()].some((poke) => poke.isOfType(Type.GRASS) && poke.isGrounded()); }) - .attr(StatChangeAttr, [BattleStat.ATK, BattleStat.SPATK], 1, false, (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded()), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded()), new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6) .attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB) .target(MoveTarget.ENEMY_SIDE), new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6) - .attr(PostVictoryStatChangeAttr, BattleStat.ATK, 3, true ), + .attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ), new AttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) .attr(ChargeAttr, ChargeAnim.PHANTOM_FORCE_CHARGING, i18next.t("moveTriggers:vanishedInstantly", {pokemonName: "{USER}"}), BattlerTagType.HIDDEN) .ignoresProtect() @@ -8017,7 +8135,7 @@ export function initMoves() { .attr(AddTypeAttr, Type.GHOST) .partial(), new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], -1) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1) .soundBased(), new StatusMove(Moves.ION_DELUGE, Type.ELECTRIC, -1, 25, -1, 1, 6) .target(MoveTarget.BOTH_SIDES) @@ -8041,7 +8159,7 @@ export function initMoves() { .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), 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(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, null, true, true, MoveEffectTrigger.PRE_APPLY) .attr(ForceSwitchOutAttr, true, false) .soundBased(), new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6) @@ -8055,7 +8173,7 @@ export function initMoves() { .attr(AddArenaTagAttr, ArenaTagType.CRAFTY_SHIELD, 1, true, true), new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6) .target(MoveTarget.ALL) - .attr(StatChangeAttr, BattleStat.DEF, 1, false, (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag)), + .attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag)), new StatusMove(Moves.GRASSY_TERRAIN, Type.GRASS, -1, 10, -1, 0, 6) .attr(TerrainChangeAttr, TerrainType.GRASSY) .target(MoveTarget.BOTH_SIDES), @@ -8065,11 +8183,11 @@ export function initMoves() { new StatusMove(Moves.ELECTRIFY, Type.ELECTRIC, -1, 20, -1, 0, 6) .unimplemented(), new AttackMove(Moves.PLAY_ROUGH, Type.FAIRY, MoveCategory.PHYSICAL, 90, 90, 10, 10, 0, 6) - .attr(StatChangeAttr, BattleStat.ATK, -1), + .attr(StatStageChangeAttr, [ Stat.ATK ], -1), new AttackMove(Moves.FAIRY_WIND, Type.FAIRY, MoveCategory.SPECIAL, 40, 100, 30, -1, 0, 6) .windMove(), new AttackMove(Moves.MOONBLAST, Type.FAIRY, MoveCategory.SPECIAL, 95, 100, 15, 30, 0, 6) - .attr(StatChangeAttr, BattleStat.SPATK, -1), + .attr(StatStageChangeAttr, [ Stat.SPATK ], -1), new AttackMove(Moves.BOOMBURST, Type.NORMAL, MoveCategory.SPECIAL, 140, 100, 10, -1, 0, 6) .soundBased() .target(MoveTarget.ALL_NEAR_OTHERS), @@ -8079,12 +8197,12 @@ export function initMoves() { 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, -1, 0, 6) - .attr(StatChangeAttr, BattleStat.ATK, -1), + .attr(StatStageChangeAttr, [ Stat.ATK ], -1), new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6) - .attr(StatChangeAttr, BattleStat.SPATK, -1) + .attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .soundBased(), new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6) - .attr(StatChangeAttr, BattleStat.DEF, 2, true) + .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true) .makesContact(false) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.STEAM_ERUPTION, Type.WATER, MoveCategory.SPECIAL, 110, 95, 5, 30, 0, 6) @@ -8098,26 +8216,26 @@ export function initMoves() { .attr(WaterShurikenPowerAttr) .attr(WaterShurikenMultiHitTypeAttr), new AttackMove(Moves.MYSTICAL_FIRE, Type.FIRE, MoveCategory.SPECIAL, 75, 100, 10, 100, 0, 6) - .attr(StatChangeAttr, BattleStat.SPATK, -1), + .attr(StatStageChangeAttr, [ Stat.SPATK ], -1), new SelfStatusMove(Moves.SPIKY_SHIELD, Type.GRASS, -1, 10, -1, 4, 6) .attr(ProtectAttr, BattlerTagType.SPIKY_SHIELD), new StatusMove(Moves.AROMATIC_MIST, Type.FAIRY, -1, 20, -1, 0, 6) - .attr(StatChangeAttr, BattleStat.SPDEF, 1) + .attr(StatStageChangeAttr, [ Stat.SPDEF ], 1) .target(MoveTarget.NEAR_ALLY), new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) - .attr(StatChangeAttr, BattleStat.SPATK, -2), + .attr(StatStageChangeAttr, [ Stat.SPATK ], -2), 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) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.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) .powderMove() .unimplemented(), new SelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6) .attr(ChargeAttr, ChargeAnim.GEOMANCY_CHARGING, i18next.t("moveTriggers:isChargingPower", {pokemonName: "{USER}"})) - .attr(StatChangeAttr, [ BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 2, true) + .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true) .ignoresVirtual(), new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6) - .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS].find(a => target.hasAbility(a, false))) + .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS].find(a => target.hasAbility(a, false))) .target(MoveTarget.USER_AND_ALLIES) .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS].find(a => p.hasAbility(a, false)))), new StatusMove(Moves.HAPPY_HOUR, Type.NORMAL, -1, 30, -1, 0, 6) // No animation @@ -8132,7 +8250,7 @@ export function initMoves() { new StatusMove(Moves.HOLD_HANDS, Type.NORMAL, -1, 40, -1, 0, 6) .target(MoveTarget.NEAR_ALLY), new StatusMove(Moves.BABY_DOLL_EYES, Type.FAIRY, 100, 30, -1, 1, 6) - .attr(StatChangeAttr, BattleStat.ATK, -1), + .attr(StatStageChangeAttr, [ Stat.ATK ], -1), new AttackMove(Moves.NUZZLE, Type.ELECTRIC, MoveCategory.PHYSICAL, 20, 100, 20, 100, 0, 6) .attr(StatusEffectAttr, StatusEffect.PARALYSIS), new AttackMove(Moves.HOLD_BACK, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 6) @@ -8141,7 +8259,7 @@ export function initMoves() { .makesContact() .attr(TrapAttr, BattlerTagType.INFESTATION), new AttackMove(Moves.POWER_UP_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 20, 100, 0, 6) - .attr(StatChangeAttr, BattleStat.ATK, 1, true) + .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true) .punchingMove(), new AttackMove(Moves.OBLIVION_WING, Type.FLYING, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 6) .attr(HitHealAttr, 0.75) @@ -8172,9 +8290,9 @@ export function initMoves() { .makesContact(false) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.DRAGON_ASCENT, Type.FLYING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 6) - .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], -1, true), + .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true), new AttackMove(Moves.HYPERSPACE_FURY, Type.DARK, MoveCategory.PHYSICAL, 100, -1, 5, -1, 0, 6) - .attr(StatChangeAttr, BattleStat.DEF, -1, true) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true) .makesContact(false) .ignoresProtect(), /* Unused */ @@ -8301,13 +8419,13 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true) .makesContact(false), new AttackMove(Moves.DARKEST_LARIAT, Type.DARK, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 7) - .attr(IgnoreOpponentStatChangesAttr), + .attr(IgnoreOpponentStatStagesAttr), 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, -1, 0, 7) - .attr(StatChangeAttr, BattleStat.SPD, -1, true) + .attr(StatStageChangeAttr, [ Stat.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) @@ -8315,8 +8433,8 @@ export function initMoves() { new AttackMove(Moves.HIGH_HORSEPOWER, Type.GROUND, MoveCategory.PHYSICAL, 95, 95, 10, -1, 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) + .attr(StatStageChangeAttr, [ Stat.ATK ], -1) + .condition((user, target, move) => target.getStatStage(Stat.ATK) > -6) .triageMove(), new AttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7) .attr(SunlightChargeAttr, ChargeAnim.SOLAR_BLADE_CHARGING, i18next.t("moveTriggers:isGlowing", {pokemonName: "{USER}"})) @@ -8328,11 +8446,11 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false), new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, -1, 0, 7) .attr(StatusEffectAttr, StatusEffect.POISON) - .attr(StatChangeAttr, BattleStat.SPD, -1), + .attr(StatStageChangeAttr, [ Stat.SPD ], -1), new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7) .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS].find(a => target.hasAbility(a, false))) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS].find(a => target.hasAbility(a, false))) .target(MoveTarget.USER_AND_ALLIES) .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS].find(a => p.hasAbility(a, false)))), new AttackMove(Moves.THROAT_CHOP, Type.DARK, MoveCategory.PHYSICAL, 80, 100, 15, 100, 0, 7) @@ -8347,11 +8465,11 @@ export function initMoves() { .attr(TerrainChangeAttr, TerrainType.PSYCHIC) .target(MoveTarget.BOTH_SIDES), new AttackMove(Moves.LUNGE, Type.BUG, MoveCategory.PHYSICAL, 80, 100, 15, 100, 0, 7) - .attr(StatChangeAttr, BattleStat.ATK, -1), + .attr(StatStageChangeAttr, [ Stat.ATK ], -1), new AttackMove(Moves.FIRE_LASH, Type.FIRE, MoveCategory.PHYSICAL, 80, 100, 15, 100, 0, 7) - .attr(StatChangeAttr, BattleStat.DEF, -1), + .attr(StatStageChangeAttr, [ Stat.DEF ], -1), new AttackMove(Moves.POWER_TRIP, Type.DARK, MoveCategory.PHYSICAL, 20, 100, 10, -1, 0, 7) - .attr(StatChangeCountPowerAttr), + .attr(PositiveStatStagePowerAttr), new AttackMove(Moves.BURN_UP, Type.FIRE, MoveCategory.SPECIAL, 130, 100, 5, -1, 0, 7) .condition((user) => { const userTypes = user.getTypes(true); @@ -8362,7 +8480,7 @@ export function initMoves() { user.scene.queueMessage(i18next.t("moveTriggers:burnedItselfOut", {pokemonName: getPokemonNameWithAffix(user)})); }), new StatusMove(Moves.SPEED_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 7) - .unimplemented(), + .attr(SwapStatAttr, Stat.SPD), new AttackMove(Moves.SMART_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 70, -1, 10, -1, 0, 7), new StatusMove(Moves.PURIFY, Type.POISON, -1, 20, -1, 0, 7) .condition( @@ -8377,7 +8495,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES) .attr(SuppressAbilitiesIfActedAttr), new AttackMove(Moves.TROP_KICK, Type.GRASS, MoveCategory.PHYSICAL, 70, 100, 15, 100, 0, 7) - .attr(StatChangeAttr, BattleStat.ATK, -1), + .attr(StatStageChangeAttr, [ Stat.ATK ], -1), new StatusMove(Moves.INSTRUCT, Type.PSYCHIC, -1, 15, -1, 0, 7) .unimplemented(), new AttackMove(Moves.BEAK_BLAST, Type.FLYING, MoveCategory.PHYSICAL, 100, 100, 15, -1, -3, 7) @@ -8385,7 +8503,7 @@ export function initMoves() { .ballBombMove() .makesContact(false), new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, -1, 0, 7) - .attr(StatChangeAttr, BattleStat.DEF, -1, true, null, true, false, MoveEffectTrigger.HIT, true) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, true) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.DRAGON_HAMMER, Type.DRAGON, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 7), @@ -8419,7 +8537,7 @@ export function initMoves() { .partial() .ignoresVirtual(), new SelfStatusMove(Moves.EXTREME_EVOBOOST, Type.NORMAL, -1, 1, -1, 0, 7) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 2, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true) .ignoresVirtual(), new AttackMove(Moves.GENESIS_SUPERNOVA, Type.PSYCHIC, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7) .attr(TerrainChangeAttr, TerrainType.PSYCHIC) @@ -8431,18 +8549,18 @@ export function initMoves() { // Fails if the user was not hit by a physical attack during the turn .condition((user, target, move) => user.getTag(ShellTrapTag)?.activated === true), new AttackMove(Moves.FLEUR_CANNON, Type.FAIRY, MoveCategory.SPECIAL, 130, 90, 5, -1, 0, 7) - .attr(StatChangeAttr, BattleStat.SPATK, -2, true), + .attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true), new AttackMove(Moves.PSYCHIC_FANGS, Type.PSYCHIC, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 7) .bitingMove() .attr(RemoveScreensAttr), new AttackMove(Moves.STOMPING_TANTRUM, Type.GROUND, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 7) .attr(MovePowerMultiplierAttr, (user, target, move) => user.getLastXMoves(2)[1]?.result === MoveResult.MISS || user.getLastXMoves(2)[1]?.result === MoveResult.FAIL ? 2 : 1), new AttackMove(Moves.SHADOW_BONE, Type.GHOST, MoveCategory.PHYSICAL, 85, 100, 10, 20, 0, 7) - .attr(StatChangeAttr, BattleStat.DEF, -1) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1) .makesContact(false), new AttackMove(Moves.ACCELEROCK, Type.ROCK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 1, 7), new AttackMove(Moves.LIQUIDATION, Type.WATER, MoveCategory.PHYSICAL, 85, 100, 10, 20, 0, 7) - .attr(StatChangeAttr, BattleStat.DEF, -1), + .attr(StatStageChangeAttr, [ Stat.DEF ], -1), new AttackMove(Moves.PRISMATIC_LASER, Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, 0, 7) .attr(RechargeAttr), new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7) @@ -8454,7 +8572,7 @@ export function initMoves() { .ignoresAbilities() .partial(), new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], -1), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1), new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7) .attr(FlinchAttr), new AttackMove(Moves.NATURES_MADNESS, Type.FAIRY, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 7) @@ -8496,7 +8614,7 @@ export function initMoves() { .makesContact(false) .ignoresVirtual(), new AttackMove(Moves.CLANGOROUS_SOULBLAZE, Type.DRAGON, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES) .partial() @@ -8563,18 +8681,18 @@ export function initMoves() { .bitingMove(), new SelfStatusMove(Moves.STUFF_CHEEKS, Type.NORMAL, -1, 10, -1, 0, 8) // TODO: Stuff Cheeks should not be selectable when the user does not have a berry, see wiki .attr(EatBerryAttr) - .attr(StatChangeAttr, BattleStat.DEF, 2, true) + .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true) .condition((user) => { const userBerries = user.scene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()); return userBerries.length > 0; }) .partial(), new SelfStatusMove(Moves.NO_RETREAT, Type.FIGHTING, -1, 5, -1, 0, 8) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false) .condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== Moves.NO_RETREAT), // fails if the user is currently trapped by No Retreat new StatusMove(Moves.TAR_SHOT, Type.ROCK, 100, 15, -1, 0, 8) - .attr(StatChangeAttr, BattleStat.SPD, -1) + .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .partial(), new StatusMove(Moves.MAGIC_POWDER, Type.PSYCHIC, 100, 20, -1, 0, 8) .attr(ChangeTypeAttr, Type.PSYCHIC) @@ -8669,16 +8787,16 @@ export function initMoves() { .ignoresVirtual(), /* End Unused */ new SelfStatusMove(Moves.CLANGOROUS_SOUL, Type.DRAGON, 100, 5, -1, 0, 8) - .attr(CutHpStatBoostAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, 3) + .attr(CutHpStatStageBoostAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, 3) .soundBased() .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, -1, 0, 8) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], 2) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 2) .ignoresProtect(), new AttackMove(Moves.DRUM_BEATING, Type.GRASS, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 8) - .attr(StatChangeAttr, BattleStat.SPD, -1) + .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .makesContact(false), new AttackMove(Moves.SNAP_TRAP, Type.GRASS, MoveCategory.PHYSICAL, 35, 100, 15, -1, 0, 8) .attr(TrapAttr, BattlerTagType.SNAP_TRAP), @@ -8691,25 +8809,25 @@ export function initMoves() { .slicingMove(), new AttackMove(Moves.BEHEMOTH_BASH, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 8), new AttackMove(Moves.AURA_WHEEL, Type.ELECTRIC, MoveCategory.PHYSICAL, 110, 100, 10, 100, 0, 8) - .attr(StatChangeAttr, BattleStat.SPD, 1, true) + .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true) .makesContact(false) .attr(AuraWheelTypeAttr) .condition((user, target, move) => [user.species.speciesId, user.fusionSpecies?.speciesId].includes(Species.MORPEKO)), // Missing custom fail message new AttackMove(Moves.BREAKING_SWIPE, Type.DRAGON, MoveCategory.PHYSICAL, 60, 100, 15, 100, 0, 8) .target(MoveTarget.ALL_NEAR_ENEMIES) - .attr(StatChangeAttr, BattleStat.ATK, -1), + .attr(StatStageChangeAttr, [ Stat.ATK ], -1), new AttackMove(Moves.BRANCH_POKE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 8), new AttackMove(Moves.OVERDRIVE, Type.ELECTRIC, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 8) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.APPLE_ACID, Type.GRASS, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 8) - .attr(StatChangeAttr, BattleStat.SPDEF, -1), + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1), new AttackMove(Moves.GRAV_APPLE, Type.GRASS, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 8) - .attr(StatChangeAttr, BattleStat.DEF, -1) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTag(ArenaTagType.GRAVITY) ? 1.5 : 1) .makesContact(false), new AttackMove(Moves.SPIRIT_BREAK, Type.FAIRY, MoveCategory.PHYSICAL, 75, 100, 15, 100, 0, 8) - .attr(StatChangeAttr, BattleStat.SPATK, -1), + .attr(StatStageChangeAttr, [ Stat.SPATK ], -1), new AttackMove(Moves.STRANGE_STEAM, Type.FAIRY, MoveCategory.SPECIAL, 90, 95, 10, 20, 0, 8) .attr(ConfuseAttr), new StatusMove(Moves.LIFE_DEW, Type.WATER, -1, 10, -1, 0, 8) @@ -8733,14 +8851,14 @@ export function initMoves() { .attr(ClearTerrainAttr) .condition((user, target, move) => !!user.scene.arena.terrain), 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(StatStageChangeAttr, Stat.SPD, 1, true) // TODO: Have boosts only apply at end of move, not after every hit + //.attr(StatStageChangeAttr, Stat.DEF, -1, true) .attr(MultiHitAttr) .makesContact(false) .partial(), new AttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, 100, 0, 8) .attr(ChargeAttr, ChargeAnim.METEOR_BEAM_CHARGING, i18next.t("moveTriggers:isOverflowingWithSpacePower", {pokemonName: "{USER}"}), null, true) - .attr(StatChangeAttr, BattleStat.SPATK, 1, true) + .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) .ignoresVirtual(), new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8) .attr(ShellSideArmCategoryAttr) @@ -8761,12 +8879,12 @@ export function initMoves() { .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() !== TerrainType.NONE && user.isGrounded() ? 2 : 1) .pulseMove(), new AttackMove(Moves.SKITTER_SMACK, Type.BUG, MoveCategory.PHYSICAL, 70, 90, 10, 100, 0, 8) - .attr(StatChangeAttr, BattleStat.SPATK, -1), + .attr(StatStageChangeAttr, [ Stat.SPATK ], -1), new AttackMove(Moves.BURNING_JEALOUSY, Type.FIRE, MoveCategory.SPECIAL, 70, 100, 5, 100, 0, 8) .attr(StatusIfBoostedAttr, StatusEffect.BURN) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.LASH_OUT, Type.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8) - .attr(MovePowerMultiplierAttr, (user, target, move) => user.turnData.battleStatsDecreased ? 2 : 1), + .attr(MovePowerMultiplierAttr, (user, _target, _move) => user.turnData.statStagesDecreased ? 2 : 1), new AttackMove(Moves.POLTERGEIST, Type.GHOST, MoveCategory.PHYSICAL, 110, 90, 5, -1, 0, 8) .attr(AttackedByItemAttr) .makesContact(false), @@ -8774,7 +8892,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_OTHERS) .unimplemented(), new StatusMove(Moves.COACHING, Type.FIGHTING, -1, 10, -1, 0, 8) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF ], 1) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1) .target(MoveTarget.NEAR_ALLY), new AttackMove(Moves.FLIP_TURN, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 20, -1, 0, 8) .attr(ForceSwitchOutAttr, true, false), @@ -8810,7 +8928,7 @@ export function initMoves() { .attr(FlinchAttr) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.THUNDEROUS_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 100, 0, 8) - .attr(StatChangeAttr, BattleStat.DEF, -1), + .attr(StatStageChangeAttr, [ Stat.DEF ], -1), new AttackMove(Moves.GLACIAL_LANCE, Type.ICE, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 8) .target(MoveTarget.ALL_NEAR_ENEMIES) .makesContact(false), @@ -8822,18 +8940,18 @@ export function initMoves() { new AttackMove(Moves.DIRE_CLAW, Type.POISON, MoveCategory.PHYSICAL, 80, 100, 15, 50, 0, 8) .attr(MultiStatusEffectAttr, [StatusEffect.POISON, StatusEffect.PARALYSIS, StatusEffect.SLEEP]), new AttackMove(Moves.PSYSHIELD_BASH, Type.PSYCHIC, MoveCategory.PHYSICAL, 70, 90, 10, 100, 0, 8) - .attr(StatChangeAttr, BattleStat.DEF, 1, true), + .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), new SelfStatusMove(Moves.POWER_SHIFT, Type.NORMAL, -1, 10, -1, 0, 8) .unimplemented(), new AttackMove(Moves.STONE_AXE, Type.ROCK, MoveCategory.PHYSICAL, 65, 90, 15, 100, 0, 8) .attr(AddArenaTrapTagHitAttr, ArenaTagType.STEALTH_ROCK) .slicingMove(), new AttackMove(Moves.SPRINGTIDE_STORM, Type.FAIRY, MoveCategory.SPECIAL, 100, 80, 5, 30, 0, 8) - .attr(StatChangeAttr, BattleStat.ATK, -1) + .attr(StatStageChangeAttr, [ Stat.ATK ], -1) .windMove() .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.MYSTICAL_POWER, Type.PSYCHIC, MoveCategory.SPECIAL, 70, 90, 10, 100, 0, 8) - .attr(StatChangeAttr, BattleStat.SPATK, 1, true), + .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true), new AttackMove(Moves.RAGING_FURY, Type.FIRE, MoveCategory.PHYSICAL, 120, 100, 10, -1, 0, 8) .makesContact(false) .attr(FrenzyAttr) @@ -8849,10 +8967,10 @@ export function initMoves() { .makesContact(false) .attr(FlinchAttr), new SelfStatusMove(Moves.VICTORY_DANCE, Type.FIGHTING, -1, 10, -1, 0, 8) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPD ], 1, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPD ], 1, true) .danceMove(), new AttackMove(Moves.HEADLONG_RUSH, Type.GROUND, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 8) - .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], -1, true) + .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true) .punchingMove(), new AttackMove(Moves.BARB_BARRAGE, Type.POISON, MoveCategory.PHYSICAL, 60, 100, 10, 50, 0, 8) .makesContact(false) @@ -8860,15 +8978,15 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.POISON), new AttackMove(Moves.ESPER_WING, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 8) .attr(HighCritAttr) - .attr(StatChangeAttr, BattleStat.SPD, 1, true), + .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true), new AttackMove(Moves.BITTER_MALICE, Type.GHOST, MoveCategory.SPECIAL, 75, 100, 10, 100, 0, 8) - .attr(StatChangeAttr, BattleStat.ATK, -1), + .attr(StatStageChangeAttr, [ Stat.ATK ], -1), new SelfStatusMove(Moves.SHELTER, Type.STEEL, -1, 10, 100, 0, 8) - .attr(StatChangeAttr, BattleStat.DEF, 2, true), + .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true), new AttackMove(Moves.TRIPLE_ARROWS, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8) .makesContact(false) .attr(HighCritAttr) - .attr(StatChangeAttr, BattleStat.DEF, -1) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1) .attr(FlinchAttr) .partial(), new AttackMove(Moves.INFERNAL_PARADE, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 15, 30, 0, 8) @@ -8879,7 +8997,7 @@ export function initMoves() { .slicingMove(), new AttackMove(Moves.BLEAKWIND_STORM, Type.FLYING, MoveCategory.SPECIAL, 100, 80, 10, 30, 0, 8) .attr(StormAccuracyAttr) - .attr(StatChangeAttr, BattleStat.SPD, -1) + .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .windMove() .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.WILDBOLT_STORM, Type.ELECTRIC, MoveCategory.SPECIAL, 100, 80, 10, 20, 0, 8) @@ -8898,7 +9016,7 @@ export function initMoves() { .target(MoveTarget.USER_AND_ALLIES) .triageMove(), new SelfStatusMove(Moves.TAKE_HEART, Type.PSYCHIC, -1, 10, -1, 0, 8) - .attr(StatChangeAttr, [ BattleStat.SPATK, BattleStat.SPDEF ], 1, true) + .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF ], 1, true) .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP), /* Unused new AttackMove(Moves.G_MAX_WILDFIRE, Type.FIRE, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) @@ -9005,7 +9123,7 @@ export function initMoves() { .attr(TeraBlastCategoryAttr) .attr(TeraBlastTypeAttr) .attr(TeraBlastPowerAttr) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR)) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR)) .partial(), new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9) .attr(ProtectAttr, BattlerTagType.SILK_TRAP), @@ -9018,17 +9136,17 @@ export function initMoves() { .attr(MovePowerMultiplierAttr, (user, target, move) => 1 + Math.min(user.isPlayer() ? user.scene.currentBattle.playerFaints : user.scene.currentBattle.enemyFaints, 100)) .makesContact(false), new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9) - .attr(StatChangeAttr, BattleStat.SPDEF, -2), + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), 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, -1, 0, 9) - .attr(StatChangeAttr, BattleStat.ATK, 2) - .attr(StatChangeAttr, BattleStat.DEF, -2), + .attr(StatStageChangeAttr, [ Stat.ATK ], 2) + .attr(StatStageChangeAttr, [ Stat.DEF ], -2), new AttackMove(Moves.SPIN_OUT, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 9) - .attr(StatChangeAttr, BattleStat.SPD, -2, true), + .attr(StatStageChangeAttr, [ Stat.SPD ], -2, true), new AttackMove(Moves.POPULATION_BOMB, Type.NORMAL, MoveCategory.PHYSICAL, 20, 90, 10, -1, 0, 9) .attr(MultiHitAttr, MultiHitType._10) .slicingMove() @@ -9070,24 +9188,24 @@ export function initMoves() { new StatusMove(Moves.DOODLE, Type.NORMAL, 100, 10, -1, 0, 9) .attr(AbilityCopyAttr, true), new SelfStatusMove(Moves.FILLET_AWAY, Type.NORMAL, -1, 10, -1, 0, 9) - .attr(CutHpStatBoostAttr, [ BattleStat.ATK, BattleStat.SPATK, BattleStat.SPD ], 2, 2), + .attr(CutHpStatStageBoostAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], 2, 2), new AttackMove(Moves.KOWTOW_CLEAVE, Type.DARK, MoveCategory.PHYSICAL, 85, -1, 10, -1, 0, 9) .slicingMove(), new AttackMove(Moves.FLOWER_TRICK, Type.GRASS, MoveCategory.PHYSICAL, 70, -1, 10, 100, 0, 9) .attr(CritOnlyAttr) .makesContact(false), new AttackMove(Moves.TORCH_SONG, Type.FIRE, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9) - .attr(StatChangeAttr, BattleStat.SPATK, 1, true) + .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) .soundBased(), new AttackMove(Moves.AQUA_STEP, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 9) - .attr(StatChangeAttr, BattleStat.SPD, 1, true) + .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true) .danceMove(), new AttackMove(Moves.RAGING_BULL, Type.NORMAL, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 9) .attr(RagingBullTypeAttr) .attr(RemoveScreensAttr), new AttackMove(Moves.MAKE_IT_RAIN, Type.STEEL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) .attr(MoneyAttr) - .attr(StatChangeAttr, BattleStat.SPATK, -1, true, null, true, false, MoveEffectTrigger.HIT, true) + .attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, null, true, false, MoveEffectTrigger.HIT, true) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.PSYBLADE, Type.PSYCHIC, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 9) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.ELECTRIC && user.isGrounded() ? 1.5 : 1) @@ -9109,17 +9227,17 @@ export function initMoves() { .attr(ForceSwitchOutAttr, true, false) .target(MoveTarget.BOTH_SIDES), new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9) - .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPD ], 1, true, null, true, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true) .attr(RemoveArenaTrapAttr, true), new StatusMove(Moves.SNOWSCAPE, Type.ICE, -1, 10, -1, 0, 9) .attr(WeatherChangeAttr, WeatherType.SNOW) .target(MoveTarget.BOTH_SIDES), new AttackMove(Moves.POUNCE, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 20, 100, 0, 9) - .attr(StatChangeAttr, BattleStat.SPD, -1), + .attr(StatStageChangeAttr, [ Stat.SPD ], -1), new AttackMove(Moves.TRAILBLAZE, Type.GRASS, MoveCategory.PHYSICAL, 50, 100, 20, 100, 0, 9) - .attr(StatChangeAttr, BattleStat.SPD, 1, true), + .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true), new AttackMove(Moves.CHILLING_WATER, Type.WATER, MoveCategory.SPECIAL, 50, 100, 20, 100, 0, 9) - .attr(StatChangeAttr, BattleStat.ATK, -1), + .attr(StatStageChangeAttr, [ Stat.ATK ], -1), new AttackMove(Moves.HYPER_DRILL, Type.NORMAL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 9) .ignoresProtect(), new AttackMove(Moves.TWIN_BEAM, Type.PSYCHIC, MoveCategory.SPECIAL, 40, 100, 10, -1, 0, 9) @@ -9128,7 +9246,7 @@ export function initMoves() { .attr(HitCountPowerAttr) .punchingMove(), new AttackMove(Moves.ARMOR_CANNON, Type.FIRE, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) - .attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], -1, true), + .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true), new AttackMove(Moves.BITTER_BLADE, Type.FIRE, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 9) .attr(HitHealAttr) .slicingMove() @@ -9183,7 +9301,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES) .triageMove(), new AttackMove(Moves.SYRUP_BOMB, Type.GRASS, MoveCategory.SPECIAL, 60, 85, 10, -1, 0, 9) - .attr(StatChangeAttr, BattleStat.SPD, -1) //Temporary + .attr(StatStageChangeAttr, [ Stat.SPD ], -1) //Temporary .ballBombMove() .partial(), new AttackMove(Moves.IVY_CUDGEL, Type.GRASS, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 9) @@ -9235,7 +9353,7 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.TOXIC) ); allMoves.map(m => { - if (m.getAttrs(StatChangeAttr).some(a => a.selfTarget && a.levels < 0)) { + if (m.getAttrs(StatStageChangeAttr).some(a => a.selfTarget && a.stages < 0)) { selfStatLowerMoves.push(m.id); } }); diff --git a/src/data/nature.ts b/src/data/nature.ts index 72e5bb7863c..c614be465c3 100644 --- a/src/data/nature.ts +++ b/src/data/nature.ts @@ -1,9 +1,9 @@ -import { Stat, getStatName } from "./pokemon-stat"; import * as Utils from "../utils"; import { TextStyle, getBBCodeFrag } from "../ui/text"; import { Nature } from "#enums/nature"; import { UiTheme } from "#enums/ui-theme"; import i18next from "i18next"; +import { Stat, EFFECTIVE_STATS, getShortenedStatKey } from "#app/enums/stat"; export { Nature }; @@ -14,10 +14,9 @@ export function getNatureName(nature: Nature, includeStatEffects: boolean = fals ret = i18next.t("nature:" + ret as any); } if (includeStatEffects) { - const stats = Utils.getEnumValues(Stat).slice(1); let increasedStat: Stat | null = null; let decreasedStat: Stat | null = null; - for (const stat of stats) { + for (const stat of EFFECTIVE_STATS) { const multiplier = getNatureStatMultiplier(nature, stat); if (multiplier > 1) { increasedStat = stat; @@ -28,7 +27,7 @@ export function getNatureName(nature: Nature, includeStatEffects: boolean = fals const textStyle = forStarterSelect ? TextStyle.SUMMARY_ALT : TextStyle.WINDOW; const getTextFrag = !ignoreBBCode ? (text: string, style: TextStyle) => getBBCodeFrag(text, style, uiTheme) : (text: string, style: TextStyle) => text; if (increasedStat && decreasedStat) { - ret = `${getTextFrag(`${ret}${!forStarterSelect ? "\n" : " "}(`, textStyle)}${getTextFrag(`+${getStatName(increasedStat, true)}`, TextStyle.SUMMARY_PINK)}${getTextFrag("/", textStyle)}${getTextFrag(`-${getStatName(decreasedStat, true)}`, TextStyle.SUMMARY_BLUE)}${getTextFrag(")", textStyle)}`; + ret = `${getTextFrag(`${ret}${!forStarterSelect ? "\n" : " "}(`, textStyle)}${getTextFrag(`+${i18next.t(getShortenedStatKey(increasedStat))}`, TextStyle.SUMMARY_PINK)}${getTextFrag("/", textStyle)}${getTextFrag(`-${i18next.t(getShortenedStatKey(decreasedStat))}`, TextStyle.SUMMARY_BLUE)}${getTextFrag(")", textStyle)}`; } else { ret = getTextFrag(`${ret}${!forStarterSelect ? "\n" : " "}(-)`, textStyle); } diff --git a/src/data/pokemon-evolutions.ts b/src/data/pokemon-evolutions.ts index 315e75e53e1..6479d620182 100644 --- a/src/data/pokemon-evolutions.ts +++ b/src/data/pokemon-evolutions.ts @@ -1,7 +1,7 @@ import { Gender } from "./gender"; import { PokeballType } from "./pokeball"; import Pokemon from "../field/pokemon"; -import { Stat } from "./pokemon-stat"; +import { Stat } from "#enums/stat"; import { Type } from "./type"; import * as Utils from "../utils"; import { SpeciesFormKey } from "./pokemon-species"; diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 17f2de794ae..8930c7053a3 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -14,7 +14,7 @@ import { GrowthRate } from "./exp"; import { EvolutionLevel, SpeciesWildEvolutionDelay, pokemonEvolutions, pokemonPrevolutions } from "./pokemon-evolutions"; import { Type } from "./type"; import { LevelMoves, pokemonFormLevelMoves, pokemonFormLevelMoves as pokemonSpeciesFormLevelMoves, pokemonSpeciesLevelMoves } from "./pokemon-level-moves"; -import { Stat } from "./pokemon-stat"; +import { Stat } from "#enums/stat"; import { Variant, VariantSet, variantColorCache, variantData } from "./variant"; export enum Region { diff --git a/src/data/pokemon-stat.ts b/src/data/pokemon-stat.ts deleted file mode 100644 index 16570785a62..00000000000 --- a/src/data/pokemon-stat.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Stat } from "#enums/stat"; -import i18next from "i18next"; - -export { Stat }; - -export function getStatName(stat: Stat, shorten: boolean = false) { - let ret: string = ""; - switch (stat) { - case Stat.HP: - ret = !shorten ? i18next.t("pokemonInfo:Stat.HP") : i18next.t("pokemonInfo:Stat.HPshortened"); - break; - case Stat.ATK: - ret = !shorten ? i18next.t("pokemonInfo:Stat.ATK") : i18next.t("pokemonInfo:Stat.ATKshortened"); - break; - case Stat.DEF: - ret = !shorten ? i18next.t("pokemonInfo:Stat.DEF") : i18next.t("pokemonInfo:Stat.DEFshortened"); - break; - case Stat.SPATK: - ret = !shorten ? i18next.t("pokemonInfo:Stat.SPATK") : i18next.t("pokemonInfo:Stat.SPATKshortened"); - break; - case Stat.SPDEF: - ret = !shorten ? i18next.t("pokemonInfo:Stat.SPDEF") : i18next.t("pokemonInfo:Stat.SPDEFshortened"); - break; - case Stat.SPD: - ret = !shorten ? i18next.t("pokemonInfo:Stat.SPD") : i18next.t("pokemonInfo:Stat.SPDshortened"); - break; - } - return ret; -} diff --git a/src/data/temp-battle-stat.ts b/src/data/temp-battle-stat.ts deleted file mode 100644 index 2d461a1d647..00000000000 --- a/src/data/temp-battle-stat.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { BattleStat, getBattleStatName } from "./battle-stat"; -import i18next from "i18next"; - -export enum TempBattleStat { - ATK, - DEF, - SPATK, - SPDEF, - SPD, - ACC, - CRIT -} - -export function getTempBattleStatName(tempBattleStat: TempBattleStat) { - if (tempBattleStat === TempBattleStat.CRIT) { - return i18next.t("modifierType:TempBattleStatBoosterStatName.CRIT"); - } - return getBattleStatName(tempBattleStat as integer as BattleStat); -} - -export function getTempBattleStatBoosterItemName(tempBattleStat: TempBattleStat) { - switch (tempBattleStat) { - case TempBattleStat.ATK: - return "X Attack"; - case TempBattleStat.DEF: - return "X Defense"; - case TempBattleStat.SPATK: - return "X Sp. Atk"; - case TempBattleStat.SPDEF: - return "X Sp. Def"; - case TempBattleStat.SPD: - return "X Speed"; - case TempBattleStat.ACC: - return "X Accuracy"; - case TempBattleStat.CRIT: - return "Dire Hit"; - } -} diff --git a/src/enums/stat.ts b/src/enums/stat.ts index a40319664d6..a12d53e8559 100644 --- a/src/enums/stat.ts +++ b/src/enums/stat.ts @@ -1,8 +1,75 @@ +/** Enum that comprises all possible stat-related attributes, in-battle and permanent, of a Pokemon. */ export enum Stat { + /** Hit Points */ HP = 0, + /** Attack */ ATK, + /** Defense */ DEF, + /** Special Attack */ SPATK, + /** Special Defense */ SPDEF, + /** Speed */ SPD, + /** Accuracy */ + ACC, + /** Evasiveness */ + EVA +} + +/** A constant array comprised of the {@linkcode Stat} values that make up {@linkcode PermanentStat}. */ +export const PERMANENT_STATS = [ Stat.HP, Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ] as const; +/** Type used to describe the core, permanent stats of a Pokemon. */ +export type PermanentStat = typeof PERMANENT_STATS[number]; + +/** A constant array comprised of the {@linkcode Stat} values that make up {@linkcode EFfectiveStat}. */ +export const EFFECTIVE_STATS = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ] as const; +/** Type used to describe the intersection of core stats and stats that have stages in battle. */ +export type EffectiveStat = typeof EFFECTIVE_STATS[number]; + +/** A constant array comprised of {@linkcode Stat} the values that make up {@linkcode BattleStat}. */ +export const BATTLE_STATS = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD, Stat.ACC, Stat.EVA ] as const; +/** Type used to describe the stats that have stages which can be incremented and decremented in battle. */ +export type BattleStat = typeof BATTLE_STATS[number]; + +/** A constant array comprised of {@linkcode Stat} the values that make up {@linkcode TempBattleStat}. */ +export const TEMP_BATTLE_STATS = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD, Stat.ACC ] as const; +/** Type used to describe the stats that have X item (`TEMP_STAT_STAGE_BOOSTER`) equivalents. */ +export type TempBattleStat = typeof TEMP_BATTLE_STATS[number]; + +/** + * Provides the translation key corresponding to the amount of stat stages and whether those stat stages + * are positive or negative. + * @param stages the amount of stages + * @param isIncrease dictates a negative (`false`) or a positive (`true`) stat stage change + * @returns the translation key fitting the conditions described by {@linkcode stages} and {@linkcode isIncrease} + */ +export function getStatStageChangeDescriptionKey(stages: number, isIncrease: boolean) { + if (stages === 1) { + return isIncrease ? "battle:statRose" : "battle:statFell"; + } else if (stages === 2) { + return isIncrease ? "battle:statSharplyRose" : "battle:statHarshlyFell"; + } else if (stages <= 6) { + return isIncrease ? "battle:statRoseDrastically" : "battle:statSeverelyFell"; + } + return isIncrease ? "battle:statWontGoAnyHigher" : "battle:statWontGoAnyLower"; +} + +/** + * Provides the translation key corresponding to a given stat which can be translated into its full name. + * @param stat the {@linkcode Stat} to be translated + * @returns the translation key corresponding to the given {@linkcode Stat} + */ +export function getStatKey(stat: Stat) { + return `pokemonInfo:Stat.${Stat[stat]}`; +} + +/** + * Provides the translation key corresponding to a given stat which can be translated into its shortened name. + * @param stat the {@linkcode Stat} to be translated + * @returns the translation key corresponding to the given {@linkcode Stat} + */ +export function getShortenedStatKey(stat: PermanentStat) { + return `pokemonInfo:Stat.${Stat[stat]}shortened`; } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index e303b599973..405a26d4a16 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3,26 +3,24 @@ import BattleScene, { AnySound } from "../battle-scene"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { variantData } from "#app/data/variant"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info"; -import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr } from "../data/move"; +import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr } from "../data/move"; import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species"; import { Constructor } from "#app/utils"; import * as Utils from "../utils"; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "../data/type"; import { getLevelTotalExp } from "../data/exp"; -import { Stat } from "../data/pokemon-stat"; -import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, PokemonBaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempBattleStatBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier } from "../modifier/modifier"; +import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; +import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier } from "../modifier/modifier"; import { PokeballType } from "../data/pokeball"; import { Gender } from "../data/gender"; import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims"; import { Status, StatusEffect, getRandomStatus } from "../data/status-effect"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; -import { BattleStat } from "../data/battle-stat"; import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; -import { TempBattleStat } from "../data/temp-battle-stat"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag"; -import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability"; +import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; @@ -40,7 +38,7 @@ import Overrides from "#app/overrides"; import i18next from "i18next"; import { speciesEggMoves } from "../data/egg-moves"; import { ModifierTier } from "../modifier/modifier-tier"; -import { applyChallenges, ChallengeType } from "#app/data/challenge.js"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattleSpec } from "#enums/battle-spec"; @@ -49,17 +47,17 @@ import { BerryType } from "#enums/berry-type"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { DamagePhase } from "#app/phases/damage-phase"; +import { FaintPhase } from "#app/phases/faint-phase"; +import { LearnMovePhase } from "#app/phases/learn-move-phase"; +import { MoveEffectPhase } from "#app/phases/move-effect-phase"; +import { MoveEndPhase } from "#app/phases/move-end-phase"; +import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; +import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase"; import { Challenges } from "#enums/challenges"; -import { getPokemonNameWithAffix } from "#app/messages.js"; -import { DamagePhase } from "#app/phases/damage-phase.js"; -import { FaintPhase } from "#app/phases/faint-phase.js"; -import { LearnMovePhase } from "#app/phases/learn-move-phase.js"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase.js"; -import { MoveEndPhase } from "#app/phases/move-end-phase.js"; -import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase.js"; -import { StatChangePhase } from "#app/phases/stat-change-phase.js"; -import { SwitchSummonPhase } from "#app/phases/switch-summon-phase.js"; -import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase.js"; export enum FieldPosition { CENTER, @@ -676,49 +674,139 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { }); } - getStat(stat: Stat): integer { + /** + * Retrieves the entire set of stats of the {@linkcode Pokemon}. + * @param bypassSummonData prefer actual stats (`true` by default) or in-battle overriden stats (`false`) + * @returns the numeric values of the {@linkcode Pokemon}'s stats + */ + getStats(bypassSummonData: boolean = true): number[] { + if (!bypassSummonData && this.summonData?.stats) { + return this.summonData.stats; + } + return this.stats; + } + + /** + * Retrieves the corresponding {@linkcode PermanentStat} of the {@linkcode Pokemon}. + * @param stat the desired {@linkcode PermanentStat} + * @param bypassSummonData prefer actual stats (`true` by default) or in-battle overridden stats (`false`) + * @returns the numeric value of the desired {@linkcode Stat} + */ + getStat(stat: PermanentStat, bypassSummonData: boolean = true): number { + if (!bypassSummonData && this.summonData && (this.summonData.stats[stat] !== 0)) { + return this.summonData.stats[stat]; + } return this.stats[stat]; } - getBattleStat(stat: Stat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): integer { - if (stat === Stat.HP) { - return this.getStat(Stat.HP); - } - const battleStat = (stat - 1) as BattleStat; - const statLevel = new Utils.IntegerHolder(this.summonData.battleStats[battleStat]); - if (opponent) { - if (isCritical) { - switch (stat) { - case Stat.ATK: - case Stat.SPATK: - statLevel.value = Math.max(statLevel.value, 0); - break; - case Stat.DEF: - case Stat.SPDEF: - statLevel.value = Math.min(statLevel.value, 0); - break; - } - } - applyAbAttrs(IgnoreOpponentStatChangesAbAttr, opponent, null, false, statLevel); - if (move) { - applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, opponent, move, statLevel); + /** + * Writes the value to the corrseponding {@linkcode PermanentStat} of the {@linkcode Pokemon}. + * + * Note that this does nothing if {@linkcode value} is less than 0. + * @param stat the desired {@linkcode PermanentStat} to be overwritten + * @param value the desired numeric value + * @param bypassSummonData write to actual stats (`true` by default) or in-battle overridden stats (`false`) + */ + setStat(stat: PermanentStat, value: number, bypassSummonData: boolean = true): void { + if (value >= 0) { + if (!bypassSummonData && this.summonData) { + this.summonData.stats[stat] = value; + } else { + this.stats[stat] = value; } } - if (this.isPlayer()) { - this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), battleStat as integer as TempBattleStat, statLevel); + } + + /** + * Retrieves the entire set of in-battle stat stages of the {@linkcode Pokemon}. + * @returns the numeric values of the {@linkcode Pokemon}'s in-battle stat stages if available, a fresh stat stage array otherwise + */ + getStatStages(): number[] { + return this.summonData ? this.summonData.statStages : [ 0, 0, 0, 0, 0, 0, 0 ]; + } + + /** + * Retrieves the in-battle stage of the specified {@linkcode BattleStat}. + * @param stat the {@linkcode BattleStat} whose stage is desired + * @returns the stage of the desired {@linkcode BattleStat} if available, 0 otherwise + */ + getStatStage(stat: BattleStat): number { + return this.summonData ? this.summonData.statStages[stat - 1] : 0; + } + + /** + * Writes the value to the in-battle stage of the corresponding {@linkcode BattleStat} of the {@linkcode Pokemon}. + * + * Note that, if the value is not within a range of [-6, 6], it will be forced to the closest range bound. + * @param stat the {@linkcode BattleStat} whose stage is to be overwritten + * @param value the desired numeric value + */ + setStatStage(stat: BattleStat, value: number): void { + if (this.summonData) { + if (value >= -6) { + this.summonData.statStages[stat - 1] = Math.min(value, 6); + } else { + this.summonData.statStages[stat - 1] = Math.max(value, -6); + } } - const statValue = new Utils.NumberHolder(this.getStat(stat)); + } + + /** + * Retrieves the critical-hit stage considering the move used and the Pokemon + * who used it. + * @param source the {@linkcode Pokemon} who using the move + * @param move the {@linkcode Move} being used + * @returns the final critical-hit stage value + */ + getCritStage(source: Pokemon, move: Move): number { + const critStage = new Utils.IntegerHolder(0); + applyMoveAttrs(HighCritAttr, source, this, move, critStage); + this.scene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage); + this.scene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage); + const bonusCrit = new Utils.BooleanHolder(false); + //@ts-ignore + if (applyAbAttrs(BonusCritAbAttr, source, null, false, bonusCrit)) { // TODO: resolve ts-ignore. This is a promise. Checking a promise is bogus. + if (bonusCrit.value) { + critStage.value += 1; + } + } + const critBoostTag = source.getTag(CritBoostTag); + if (critBoostTag) { + if (critBoostTag instanceof DragonCheerTag) { + critStage.value += critBoostTag.typesOnAdd.includes(Type.DRAGON) ? 2 : 1; + } else { + critStage.value += 2; + } + } + + console.log(`crit stage: +${critStage.value}`); + return critStage.value; + } + + /** + * Calculates and retrieves the final value of a stat considering any held + * items, move effects, opponent abilities, and whether there was a critical + * hit. + * @param stat the desired {@linkcode EffectiveStat} + * @param opponent the target {@linkcode Pokemon} + * @param move the {@linkcode Move} being used + * @param isCritical determines whether a critical hit has occurred or not (`false` by default) + * @returns the final in-battle value of a stat + */ + getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): integer { + const statValue = new Utils.NumberHolder(this.getStat(stat, false)); this.scene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue); const fieldApplied = new Utils.BooleanHolder(false); for (const pokemon of this.scene.getField(true)) { - applyFieldBattleStatMultiplierAbAttrs(FieldMultiplyBattleStatAbAttr, pokemon, stat, statValue, this, fieldApplied); + applyFieldStatMultiplierAbAttrs(FieldMultiplyStatAbAttr, pokemon, stat, statValue, this, fieldApplied); if (fieldApplied.value) { break; } } - applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, battleStat, statValue); - let ret = statValue.value * (Math.max(2, 2 + statLevel.value) / Math.max(2, 2 - statLevel.value)); + applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, stat, statValue); + let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, isCritical); + switch (stat) { case Stat.ATK: if (this.getTag(BattlerTagType.SLOW_START)) { @@ -765,24 +853,25 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!this.stats) { this.stats = [ 0, 0, 0, 0, 0, 0 ]; } - const baseStats = this.getSpeciesForm().baseStats.slice(0); - if (this.fusionSpecies) { - const fusionBaseStats = this.getFusionSpeciesForm().baseStats; - for (let s = 0; s < this.stats.length; s++) { + + // Get and manipulate base stats + const baseStats = this.getSpeciesForm(true).baseStats.slice(); + if (this.isFusion()) { + const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats; + for (const s of PERMANENT_STATS) { baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2); } } else if (this.scene.gameMode.isSplicedOnly) { - for (let s = 0; s < this.stats.length; s++) { + for (const s of PERMANENT_STATS) { baseStats[s] = Math.ceil(baseStats[s] / 2); } } - this.scene.applyModifiers(PokemonBaseStatModifier, this.isPlayer(), this, baseStats); - const stats = Utils.getEnumValues(Stat); - for (const s of stats) { - const isHp = s === Stat.HP; - const baseStat = baseStats[s]; - let value = Math.floor(((2 * baseStat + this.ivs[s]) * this.level) * 0.01); - if (isHp) { + this.scene.applyModifiers(BaseStatModifier, this.isPlayer(), this, baseStats); + + // Using base stats, calculate and store stats one by one + for (const s of PERMANENT_STATS) { + let value = Math.floor(((2 * baseStats[s] + this.ivs[s]) * this.level) * 0.01); + if (s === Stat.HP) { value = value + this.level + 10; if (this.hasAbility(Abilities.WONDER_GUARD, false, true)) { value = 1; @@ -803,7 +892,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { value = Math.max(Math[natureStatMultiplier.value > 1 ? "ceil" : "floor"](value * natureStatMultiplier.value), 1); } } - this.stats[s] = value; + + this.setStat(s, value); } } @@ -1378,7 +1468,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const types = this.getTypes(true); const enemyTypes = opponent.getTypes(true, true); /** Is this Pokemon faster than the opponent? */ - const outspeed = (this.isActive(true) ? this.getBattleStat(Stat.SPD, opponent) : this.getStat(Stat.SPD)) >= opponent.getBattleStat(Stat.SPD, this); + const outspeed = (this.isActive(true) ? this.getEffectiveStat(Stat.SPD, opponent) : this.getStat(Stat.SPD, false)) >= opponent.getEffectiveStat(Stat.SPD, this); /** * Based on how effective this Pokemon's types are offensively against the opponent's types. * This score is increased by 25 percent if this Pokemon is faster than the opponent. @@ -1757,7 +1847,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(SacrificialAttr) ? 0.5 : 1)]); movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(SacrificialAttrOnHit) ? 0.5 : 1)]); // Trainers get a weight bump to stat buffing moves - movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].getAttrs(StatChangeAttr).some(a => a.levels > 1 && a.selfTarget) ? 1.25 : 1)]); + movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].getAttrs(StatStageChangeAttr).some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1)]); // Trainers get a weight decrease to multiturn moves movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(ChargeAttr) || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1)]); } @@ -1769,8 +1859,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === MoveCategory.STATUS ? 1 : Math.max(Math.min(allMoves[m[0]].power/maxPower, 1), 0.5))]); // Weight damaging moves against the lower stat - const worseCategory: MoveCategory = this.stats[Stat.ATK] > this.stats[Stat.SPATK] ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL; - const statRatio = worseCategory === MoveCategory.PHYSICAL ? this.stats[Stat.ATK]/this.stats[Stat.SPATK] : this.stats[Stat.SPATK]/this.stats[Stat.ATK]; + const atk = this.getStat(Stat.ATK); + const spAtk = this.getStat(Stat.SPATK); + const worseCategory: MoveCategory = atk > spAtk ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL; + const statRatio = worseCategory === MoveCategory.PHYSICAL ? atk / spAtk : spAtk / atk; movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === worseCategory ? statRatio : 1)]); let weightMultiplier = 0.9; // The higher this is the more the game weights towards higher level moves. At 0 all moves are equal weight. @@ -1956,6 +2048,48 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this instanceof PlayerPokemon ? this.scene.getPlayerField() : this.scene.getEnemyField(); } + /** + * Calculates the stat stage multiplier of the user against an opponent. + * + * Note that this does not apply to evasion or accuracy + * @see {@linkcode getAccuracyMultiplier} + * @param stat the desired {@linkcode EffectiveStat} + * @param opponent the target {@linkcode Pokemon} + * @param move the {@linkcode Move} being used + * @param isCritical determines whether a critical hit has occurred or not (`false` by default) + * @return the stat stage multiplier to be used for effective stat calculation + */ + getStatStageMultiplier(stat: EffectiveStat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): number { + const statStage = new Utils.IntegerHolder(this.getStatStage(stat)); + const ignoreStatStage = new Utils.BooleanHolder(false); + + if (opponent) { + if (isCritical) { + switch (stat) { + case Stat.ATK: + case Stat.SPATK: + statStage.value = Math.max(statStage.value, 0); + break; + case Stat.DEF: + case Stat.SPDEF: + statStage.value = Math.min(statStage.value, 0); + break; + } + } + applyAbAttrs(IgnoreOpponentStatStagesAbAttr, opponent, null, false, stat, ignoreStatStage); + if (move) { + applyMoveAttrs(IgnoreOpponentStatStagesAttr, this, opponent, move, ignoreStatStage); + } + } + + if (!ignoreStatStage.value) { + const statStageMultiplier = new Utils.NumberHolder(Math.max(2, 2 + statStage.value) / Math.max(2, 2 - statStage.value)); + this.scene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier); + return Math.min(statStageMultiplier.value, 4); + } + return 1; + } + /** * Calculates the accuracy multiplier of the user against a target. * @@ -1972,34 +2106,38 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return 1; } - const userAccuracyLevel = new Utils.IntegerHolder(this.summonData.battleStats[BattleStat.ACC]); - const targetEvasionLevel = new Utils.IntegerHolder(target.summonData.battleStats[BattleStat.EVA]); + const userAccStage = new Utils.IntegerHolder(this.getStatStage(Stat.ACC)); + const targetEvaStage = new Utils.IntegerHolder(target.getStatStage(Stat.EVA)); - applyAbAttrs(IgnoreOpponentStatChangesAbAttr, target, null, false, userAccuracyLevel); - applyAbAttrs(IgnoreOpponentStatChangesAbAttr, this, null, false, targetEvasionLevel); - applyAbAttrs(IgnoreOpponentEvasionAbAttr, this, null, false, targetEvasionLevel); - applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, target, sourceMove, targetEvasionLevel); - this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), TempBattleStat.ACC, userAccuracyLevel); + const ignoreAccStatStage = new Utils.BooleanHolder(false); + const ignoreEvaStatStage = new Utils.BooleanHolder(false); + + applyAbAttrs(IgnoreOpponentStatStagesAbAttr, target, null, false, Stat.ACC, ignoreAccStatStage); + applyAbAttrs(IgnoreOpponentStatStagesAbAttr, this, null, false, Stat.EVA, ignoreEvaStatStage); + applyMoveAttrs(IgnoreOpponentStatStagesAttr, this, target, sourceMove, ignoreEvaStatStage); + + this.scene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), Stat.ACC, userAccStage); + + userAccStage.value = ignoreAccStatStage.value ? 0 : Math.min(userAccStage.value, 6); + targetEvaStage.value = ignoreEvaStatStage.value ? 0 : targetEvaStage.value; if (target.findTag(t => t instanceof ExposedTag)) { - targetEvasionLevel.value = Math.min(0, targetEvasionLevel.value); + targetEvaStage.value = Math.min(0, targetEvaStage.value); } const accuracyMultiplier = new Utils.NumberHolder(1); - if (userAccuracyLevel.value !== targetEvasionLevel.value) { - accuracyMultiplier.value = userAccuracyLevel.value > targetEvasionLevel.value - ? (3 + Math.min(userAccuracyLevel.value - targetEvasionLevel.value, 6)) / 3 - : 3 / (3 + Math.min(targetEvasionLevel.value - userAccuracyLevel.value, 6)); + if (userAccStage.value !== targetEvaStage.value) { + accuracyMultiplier.value = userAccStage.value > targetEvaStage.value + ? (3 + Math.min(userAccStage.value - targetEvaStage.value, 6)) / 3 + : 3 / (3 + Math.min(targetEvaStage.value - userAccStage.value, 6)); } - applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, BattleStat.ACC, accuracyMultiplier, false, sourceMove); + applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, Stat.ACC, accuracyMultiplier, false, sourceMove); const evasionMultiplier = new Utils.NumberHolder(1); - applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, target, BattleStat.EVA, evasionMultiplier); + applyStatMultiplierAbAttrs(StatMultiplierAbAttr, target, Stat.EVA, evasionMultiplier); - accuracyMultiplier.value /= evasionMultiplier.value; - - return accuracyMultiplier.value; + return accuracyMultiplier.value / evasionMultiplier.value; } /** @@ -2086,29 +2224,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (critOnly.value || critAlways) { isCritical = true; } else { - const critLevel = new Utils.IntegerHolder(0); - applyMoveAttrs(HighCritAttr, source, this, move, critLevel); - this.scene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critLevel); - this.scene.applyModifiers(TempBattleStatBoosterModifier, source.isPlayer(), TempBattleStat.CRIT, critLevel); - const bonusCrit = new Utils.BooleanHolder(false); - //@ts-ignore - if (applyAbAttrs(BonusCritAbAttr, source, null, false, bonusCrit)) { // TODO: resolve ts-ignore. This is a promise. Checking a promise is bogus. - if (bonusCrit.value) { - critLevel.value += 1; - } - } - - const critBoostTag = source.getTag(CritBoostTag); - if (critBoostTag) { - if (critBoostTag instanceof DragonCheerTag) { - critLevel.value += critBoostTag.typesOnAdd.includes(Type.DRAGON) ? 2 : 1; - } else { - critLevel.value += 2; - } - } - - console.log(`crit stage: +${critLevel.value}`); - const critChance = [24, 8, 2, 1][Math.max(0, Math.min(critLevel.value, 3))]; + const critChance = [24, 8, 2, 1][Math.max(0, Math.min(this.getCritStage(source, move), 3))]; isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance); if (Overrides.NEVER_CRIT_OVERRIDE) { isCritical = false; @@ -2122,8 +2238,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { isCritical = false; } } - const sourceAtk = new Utils.IntegerHolder(source.getBattleStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, isCritical)); - const targetDef = new Utils.IntegerHolder(this.getBattleStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, isCritical)); + const sourceAtk = new Utils.IntegerHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, isCritical)); + const targetDef = new Utils.IntegerHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, isCritical)); const criticalMultiplier = new Utils.NumberHolder(isCritical ? 1.5 : 1); applyAbAttrs(MultCritAbAttr, source, null, false, criticalMultiplier); const screenMultiplier = new Utils.NumberHolder(1); @@ -2534,10 +2650,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param source {@linkcode Pokemon} the pokemon whose stats/Tags are to be passed on from, ie: the Pokemon using Baton Pass */ transferSummon(source: Pokemon): void { - const battleStats = Utils.getEnumValues(BattleStat); - for (const stat of battleStats) { - this.summonData.battleStats[stat] = source.summonData.battleStats[stat]; + // Copy all stat stages + for (const s of BATTLE_STATS) { + const sourceStage = source.getStatStage(s); + if ((this instanceof PlayerPokemon) && (sourceStage === 6)) { + this.scene.validateAchv(achvs.TRANSFER_MAX_STAT_STAGE); + } + this.setStatStage(s, sourceStage); } + for (const tag of source.summonData.tags) { // bypass those can not be passed via Baton Pass @@ -2549,9 +2670,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.summonData.tags.push(tag); } - if (this instanceof PlayerPokemon && source.summonData.battleStats.find(bs => bs === 6)) { - this.scene.validateAchv(achvs.TRANSFER_MAX_BATTLE_STAT); - } + this.updateInfo(); } @@ -3729,16 +3848,17 @@ export class PlayerPokemon extends Pokemon { this.scene.gameData.gameStats.pokemonFused++; // Store the average HP% that each Pokemon has - const newHpPercent = ((pokemon.hp / pokemon.stats[Stat.HP]) + (this.hp / this.stats[Stat.HP])) / 2; + const maxHp = this.getMaxHp(); + const newHpPercent = ((pokemon.hp / pokemon.getMaxHp()) + (this.hp / maxHp)) / 2; this.generateName(); this.calculateStats(); // Set this Pokemon's HP to the average % of both fusion components - this.hp = Math.round(this.stats[Stat.HP] * newHpPercent); + this.hp = Math.round(maxHp * newHpPercent); if (!this.isFainted()) { // If this Pokemon hasn't fainted, make sure the HP wasn't set over the new maximum - this.hp = Math.min(this.hp, this.stats[Stat.HP]); + this.hp = Math.min(this.hp, maxHp); this.status = getRandomStatus(this.status, pokemon.status); // Get a random valid status between the two } else if (!pokemon.isFainted()) { // If this Pokemon fainted but the other hasn't, make sure the HP wasn't set to zero @@ -4231,39 +4351,40 @@ export class EnemyPokemon extends Pokemon { handleBossSegmentCleared(segmentIndex: integer): void { while (segmentIndex - 1 < this.bossSegmentIndex) { - let boostedStat = BattleStat.RAND; + // Filter out already maxed out stat stages and weigh the rest based on existing stats + const leftoverStats = EFFECTIVE_STATS.filter((s: EffectiveStat) => this.getStatStage(s) < 6); + const statWeights = leftoverStats.map((s: EffectiveStat) => this.getStat(s, false)); - const battleStats = Utils.getEnumValues(BattleStat).slice(0, -3); - const statWeights = new Array().fill(battleStats.length).filter((bs: BattleStat) => this.summonData.battleStats[bs] < 6).map((bs: BattleStat) => this.getStat(bs + 1)); - const statThresholds: integer[] = []; + let boostedStat: EffectiveStat; + const statThresholds: number[] = []; let totalWeight = 0; - for (const bs of battleStats) { - totalWeight += statWeights[bs]; + + for (const i in statWeights) { + totalWeight += statWeights[i]; statThresholds.push(totalWeight); } + // Pick a random stat from the leftover stats to increase its stages const randInt = Utils.randSeedInt(totalWeight); - - for (const bs of battleStats) { - if (randInt < statThresholds[bs]) { - boostedStat = bs; + for (const i in statThresholds) { + if (randInt < statThresholds[i]) { + boostedStat = leftoverStats[i]; break; } } - let statLevels = 1; + let stages = 1; // increase the boost if the boss has at least 3 segments and we passed last shield if (this.bossSegments >= 3 && this.bossSegmentIndex === 1) { - statLevels++; + stages++; } // increase the boost if the boss has at least 5 segments and we passed the second to last shield if (this.bossSegments >= 5 && this.bossSegmentIndex === 2) { - statLevels++; + stages++; } - this.scene.unshiftPhase(new StatChangePhase(this.scene, this.getBattlerIndex(), true, [ boostedStat ], statLevels, true, true)); - + this.scene.unshiftPhase(new StatStageChangePhase(this.scene, this.getBattlerIndex(), true, [ boostedStat! ], stages, true, true)); this.bossSegmentIndex--; } } @@ -4339,7 +4460,7 @@ export interface AttackMoveResult { } export class PokemonSummonData { - public battleStats: number[] = [ 0, 0, 0, 0, 0, 0, 0 ]; + public statStages: number[] = [ 0, 0, 0, 0, 0, 0, 0 ]; public moveQueue: QueuedMove[] = []; public disabledMove: Moves = Moves.NONE; public disabledTurns: number = 0; @@ -4352,7 +4473,7 @@ export class PokemonSummonData { public ability: Abilities = Abilities.NONE; public gender: Gender; public fusionGender: Gender; - public stats: number[]; + public stats: number[] = [ 0, 0, 0, 0, 0, 0 ]; public moveset: (PokemonMove | null)[]; // If not initialized this value will not be populated from save data. public types: Type[] = []; @@ -4383,8 +4504,8 @@ export class PokemonTurnData { public damageTaken: number = 0; public attacksReceived: AttackMoveResult[] = []; public order: number; - public battleStatsIncreased: boolean = false; - public battleStatsDecreased: boolean = false; + public statStagesIncreased: boolean = false; + public statStagesDecreased: boolean = false; } export enum AiType { diff --git a/src/interfaces/locales.ts b/src/interfaces/locales.ts index 5f7c52100c1..4405095e0fe 100644 --- a/src/interfaces/locales.ts +++ b/src/interfaces/locales.ts @@ -37,8 +37,7 @@ export interface ModifierTypeTranslationEntries { ModifierType: { [key: string]: ModifierTypeTranslationEntry }, SpeciesBoosterItem: { [key: string]: ModifierTypeTranslationEntry }, AttackTypeBoosterItem: SimpleTranslationEntries, - TempBattleStatBoosterItem: SimpleTranslationEntries, - TempBattleStatBoosterStatName: SimpleTranslationEntries, + TempStatStageBoosterItem: SimpleTranslationEntries, BaseStatBoosterItem: SimpleTranslationEntries, EvolutionItem: SimpleTranslationEntries, FormChangeItem: SimpleTranslationEntries, diff --git a/src/locales/de/achv.json b/src/locales/de/achv.json index d2e56089720..21a1d89f9d6 100644 --- a/src/locales/de/achv.json +++ b/src/locales/de/achv.json @@ -89,7 +89,7 @@ "name": "Bänder-Meister", "name_female": "Bänder-Meisterin" }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { "name": "Teamwork", "description": "Nutze Staffette, während der Anwender mindestens eines Statuswertes maximiert hat." }, @@ -274,4 +274,4 @@ "name": "Spieglein, Spieglein an der Wand", "description": "Schließe die 'Umkehrkampf' Herausforderung ab" } -} \ No newline at end of file +} diff --git a/src/locales/de/modifier-type.json b/src/locales/de/modifier-type.json index 9298a78614a..8e2372cb447 100644 --- a/src/locales/de/modifier-type.json +++ b/src/locales/de/modifier-type.json @@ -49,8 +49,8 @@ "DoubleBattleChanceBoosterModifierType": { "description": "Verdoppelt die Wahrscheinlichkeit, dass die nächsten {{battleCount}} Begegnungen mit wilden Pokémon ein Doppelkampf sind." }, - "TempBattleStatBoosterModifierType": { - "description": "Erhöht die {{tempBattleStatName}} aller Teammitglieder für 5 Kämpfe um eine Stufe." + "TempStatStageBoosterModifierType": { + "description": "Erhöht die {{stat}} aller Teammitglieder für 5 Kämpfe um eine Stufe." }, "AttackTypeBoosterModifierType": { "description": "Erhöht die Stärke aller {{moveType}}-Attacken eines Pokémon um 20%." @@ -61,8 +61,8 @@ "AllPokemonLevelIncrementModifierType": { "description": "Erhöht das Level aller Teammitglieder um {{levels}}." }, - "PokemonBaseStatBoosterModifierType": { - "description": "Erhöht den {{statName}} Basiswert des Trägers um 10%. Das Stapellimit erhöht sich, je höher dein IS-Wert ist." + "BaseStatBoosterModifierType": { + "description": "Erhöht den {{stat}} Basiswert des Trägers um 10%. Das Stapellimit erhöht sich, je höher dein IS-Wert ist." }, "AllPokemonFullHpRestoreModifierType": { "description": "Stellt 100% der KP aller Pokémon her." @@ -248,6 +248,12 @@ "name": "Scope-Linse", "description": "Ein Item zum Tragen. Es erhöht die Volltrefferquote." }, + "DIRE_HIT": { + "name": "X-Volltreffer", + "extra": { + "raises": "Volltrefferquote" + } + }, "LEEK": { "name": "Lauchstange", "description": "Ein Item, das von Porenta getragen werden kann. Diese lange Lauchstange erhöht die Volltrefferquote stark." @@ -411,25 +417,13 @@ "description": "Ein Item, das Ditto zum Tragen gegeben werden kann. Fein und doch hart, erhöht dieses sonderbare Pulver die Initiative." } }, - "TempBattleStatBoosterItem": { + "TempStatStageBoosterItem": { "x_attack": "X-Angriff", "x_defense": "X-Verteidigung", "x_sp_atk": "X-Sp.-Ang.", "x_sp_def": "X-Sp.-Vert.", "x_speed": "X-Tempo", - "x_accuracy": "X-Treffer", - "dire_hit": "X-Volltreffer" - }, - "TempBattleStatBoosterStatName": { - "ATK": "Angriff", - "DEF": "Verteidigung", - "SPATK": "Sp. Ang", - "SPDEF": "Sp. Vert", - "SPD": "Initiative", - "ACC": "Genauigkeit", - "CRIT": "Volltrefferquote", - "EVA": "Fluchtwert", - "DEFAULT": "???" + "x_accuracy": "X-Treffer" }, "AttackTypeBoosterItem": { "silk_scarf": "Seidenschal", @@ -606,4 +600,4 @@ "FAIRY_MEMORY": "Feen-Disc", "NORMAL_MEMORY": "Normal-Disc" } -} \ No newline at end of file +} diff --git a/src/locales/de/modifier.json b/src/locales/de/modifier.json index 22053b1da63..37227973410 100644 --- a/src/locales/de/modifier.json +++ b/src/locales/de/modifier.json @@ -3,7 +3,7 @@ "turnHealApply": "{{typeName}} von {{pokemonNameWithAffix}} füllt einige KP auf!", "hitHealApply": "{{typeName}} von {{pokemonNameWithAffix}} füllt einige KP auf!", "pokemonInstantReviveApply": "{{pokemonNameWithAffix}} wurde durch {{typeName}} wiederbelebt!", - "pokemonResetNegativeStatStageApply": "Die negative Statuswertveränderung von {{pokemonNameWithAffix}} wurde durch {{typeName}} aufgehoben!", + "resetNegativeStatStageApply": "Die negative Statuswertveränderung von {{pokemonNameWithAffix}} wurde durch {{typeName}} aufgehoben!", "moneyInterestApply": "Du erhählst {{moneyAmount}} ₽ durch das Item {{typeName}}!", "turnHeldItemTransferApply": "{{itemName}} von {{pokemonNameWithAffix}} wurde durch {{typeName}} von {{pokemonName}} absorbiert!", "contactHeldItemTransferApply": "{{itemName}} von {{pokemonNameWithAffix}} wurde durch {{typeName}} von {{pokemonName}} geklaut!", diff --git a/src/locales/de/move-trigger.json b/src/locales/de/move-trigger.json index 163e8014d8b..61283c9e62e 100644 --- a/src/locales/de/move-trigger.json +++ b/src/locales/de/move-trigger.json @@ -3,6 +3,10 @@ "cutHpPowerUpMove": "{{pokemonName}} nutzt seine KP um seine Attacke zu verstärken!", "absorbedElectricity": "{{pokemonName}} absorbiert elektrische Energie!", "switchedStatChanges": "{{pokemonName}} tauschte die Statuswerteveränderungen mit dem Ziel!", + "switchedTwoStatChanges": "{{pokemonName}} tauscht Veränderungen an {{firstStat}} und {{secondStat}} mit dem Ziel!", + "switchedStat": "{{pokemonName}} tauscht seinen {{stat}}-Wert mit dem des Zieles!", + "sharedGuard": "{{pokemonName}} addiert seine Schutzkräfte mit jenen des Zieles und teilt sie gerecht auf!", + "sharedPower": "{{pokemonName}} addiert seine Kräfte mit jenen des Zieles und teilt sie gerecht auf!", "goingAllOutForAttack": "{{pokemonName}} legt sich ins Zeug!", "regainedHealth": "{{pokemonName}} erholt sich!", "keptGoingAndCrashed": "{{pokemonName}} springt daneben und verletzt sich!", @@ -63,4 +67,4 @@ "swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!", "exposedMove": "{{pokemonName}} erkennt {{targetPokemonName}}!", "safeguard": "{{targetName}} wird durch Bodyguard geschützt!" -} \ No newline at end of file +} diff --git a/src/locales/de/pokemon-info.json b/src/locales/de/pokemon-info.json index a559001f663..2d625d52ba7 100644 --- a/src/locales/de/pokemon-info.json +++ b/src/locales/de/pokemon-info.json @@ -1,7 +1,6 @@ { "Stat": { "HP": "KP", - "HPStat": "KP", "HPshortened": "KP", "ATK": "Angriff", "ATKshortened": "Ang", diff --git a/src/locales/en/achv.json b/src/locales/en/achv.json index fae786e034a..32d519fbf78 100644 --- a/src/locales/en/achv.json +++ b/src/locales/en/achv.json @@ -97,9 +97,9 @@ "name": "Master League Champion", "name_female": "Master League Champion" }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { "name": "Teamwork", - "description": "Baton pass to another party member with at least one stat maxed out" + "description": "Baton pass to another party member with at least one stat stage maxed out" }, "MAX_FRIENDSHIP": { "name": "Friendmaxxing", @@ -284,4 +284,4 @@ "name": "Mirror rorriM", "description": "Complete the Inverse Battle challenge.\n.egnellahc elttaB esrevnI eht etelpmoC" } -} \ No newline at end of file +} diff --git a/src/locales/en/modifier-type.json b/src/locales/en/modifier-type.json index 15b9fb8f46d..f73a3dcccae 100644 --- a/src/locales/en/modifier-type.json +++ b/src/locales/en/modifier-type.json @@ -49,8 +49,8 @@ "DoubleBattleChanceBoosterModifierType": { "description": "Doubles the chance of an encounter being a double battle for {{battleCount}} battles." }, - "TempBattleStatBoosterModifierType": { - "description": "Increases the {{tempBattleStatName}} of all party members by 1 stage for 5 battles." + "TempStatStageBoosterModifierType": { + "description": "Increases the {{stat}} of all party members by 1 stage for 5 battles." }, "AttackTypeBoosterModifierType": { "description": "Increases the power of a Pokémon's {{moveType}}-type moves by 20%." @@ -61,8 +61,8 @@ "AllPokemonLevelIncrementModifierType": { "description": "Increases all party members' level by {{levels}}." }, - "PokemonBaseStatBoosterModifierType": { - "description": "Increases the holder's base {{statName}} by 10%. The higher your IVs, the higher the stack limit." + "BaseStatBoosterModifierType": { + "description": "Increases the holder's base {{stat}} by 10%. The higher your IVs, the higher the stack limit." }, "AllPokemonFullHpRestoreModifierType": { "description": "Restores 100% HP for all Pokémon." @@ -183,6 +183,7 @@ "SOOTHE_BELL": { "name": "Soothe Bell" }, "SCOPE_LENS": { "name": "Scope Lens", "description": "It's a lens for scoping out weak points. It boosts the holder's critical-hit ratio."}, + "DIRE_HIT": { "name": "Dire Hit", "extra": { "raises": "Critical Hit Ratio" } }, "LEEK": { "name": "Leek", "description": "This very long and stiff stalk of leek boosts the critical-hit ratio of Farfetch'd's moves."}, "EVIOLITE": { "name": "Eviolite", "description": "This mysterious evolutionary lump boosts the Defense and Sp. Def stats when held by a Pokémon that can still evolve." }, @@ -250,28 +251,14 @@ "METAL_POWDER": { "name": "Metal Powder", "description": "Extremely fine yet hard, this odd powder boosts Ditto's Defense stat." }, "QUICK_POWDER": { "name": "Quick Powder", "description": "Extremely fine yet hard, this odd powder boosts Ditto's Speed stat." } }, - "TempBattleStatBoosterItem": { + "TempStatStageBoosterItem": { "x_attack": "X Attack", "x_defense": "X Defense", "x_sp_atk": "X Sp. Atk", "x_sp_def": "X Sp. Def", "x_speed": "X Speed", - "x_accuracy": "X Accuracy", - "dire_hit": "Dire Hit" + "x_accuracy": "X Accuracy" }, - - "TempBattleStatBoosterStatName": { - "ATK": "Attack", - "DEF": "Defense", - "SPATK": "Sp. Atk", - "SPDEF": "Sp. Def", - "SPD": "Speed", - "ACC": "Accuracy", - "CRIT": "Critical Hit Ratio", - "EVA": "Evasiveness", - "DEFAULT": "???" - }, - "AttackTypeBoosterItem": { "silk_scarf": "Silk Scarf", "black_belt": "Black Belt", diff --git a/src/locales/en/modifier.json b/src/locales/en/modifier.json index 473be0e8bfa..47944c8adb7 100644 --- a/src/locales/en/modifier.json +++ b/src/locales/en/modifier.json @@ -3,7 +3,7 @@ "turnHealApply": "{{pokemonNameWithAffix}} restored a little HP using\nits {{typeName}}!", "hitHealApply": "{{pokemonNameWithAffix}} restored a little HP using\nits {{typeName}}!", "pokemonInstantReviveApply": "{{pokemonNameWithAffix}} was revived\nby its {{typeName}}!", - "pokemonResetNegativeStatStageApply": "{{pokemonNameWithAffix}}'s lowered stats were restored\nby its {{typeName}}!", + "resetNegativeStatStageApply": "{{pokemonNameWithAffix}}'s lowered stats were restored\nby its {{typeName}}!", "moneyInterestApply": "You received interest of ₽{{moneyAmount}}\nfrom the {{typeName}}!", "turnHeldItemTransferApply": "{{pokemonNameWithAffix}}'s {{itemName}} was absorbed\nby {{pokemonName}}'s {{typeName}}!", "contactHeldItemTransferApply": "{{pokemonNameWithAffix}}'s {{itemName}} was snatched\nby {{pokemonName}}'s {{typeName}}!", diff --git a/src/locales/en/move-trigger.json b/src/locales/en/move-trigger.json index baddbaa34bf..110d3dc68c7 100644 --- a/src/locales/en/move-trigger.json +++ b/src/locales/en/move-trigger.json @@ -3,6 +3,10 @@ "cutHpPowerUpMove": "{{pokemonName}} cut its own HP to power up its move!", "absorbedElectricity": "{{pokemonName}} absorbed electricity!", "switchedStatChanges": "{{pokemonName}} switched stat changes with the target!", + "switchedTwoStatChanges": "{{pokemonName}} switched all changes to its {{firstStat}}\nand {{secondStat}} with its target!", + "switchedStat": "{{pokemonName}} switched {{stat}} with its target!", + "sharedGuard": "{{pokemonName}} shared its guard with the target!", + "sharedPower": "{{pokemonName}} shared its power with the target!", "goingAllOutForAttack": "{{pokemonName}} is going all out for this attack!", "regainedHealth": "{{pokemonName}} regained\nhealth!", "keptGoingAndCrashed": "{{pokemonName}} kept going\nand crashed!", diff --git a/src/locales/en/pokemon-info.json b/src/locales/en/pokemon-info.json index 87d2f7ad17b..b79daaed621 100644 --- a/src/locales/en/pokemon-info.json +++ b/src/locales/en/pokemon-info.json @@ -1,7 +1,7 @@ { "Stat": { "HP": "Max. HP", - "HPshortened": "MaxHP", + "HPshortened": "HP", "ATK": "Attack", "ATKshortened": "Atk", "DEF": "Defense", @@ -13,8 +13,7 @@ "SPD": "Speed", "SPDshortened": "Spd", "ACC": "Accuracy", - "EVA": "Evasiveness", - "HPStat": "HP" + "EVA": "Evasiveness" }, "Type": { "UNKNOWN": "Unknown", @@ -38,4 +37,4 @@ "FAIRY": "Fairy", "STELLAR": "Stellar" } -} \ No newline at end of file +} diff --git a/src/locales/es/achv.json b/src/locales/es/achv.json index c94b8858233..14501dbdb6b 100644 --- a/src/locales/es/achv.json +++ b/src/locales/es/achv.json @@ -91,7 +91,7 @@ "name": "Campeón Liga Master", "name_female": "Campeona Liga Master" }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { "name": "Trabajo en Equipo", "description": "Haz relevo a otro miembro del equipo con al menos una estadística al máximo." }, @@ -175,4 +175,4 @@ "name": "Espejo ojepsE", "description": "Completa el reto de Combate Inverso.\n.osrevnI etabmoC ed oter le atelpmoC" } -} \ No newline at end of file +} diff --git a/src/locales/es/modifier-type.json b/src/locales/es/modifier-type.json index 9c36b8da767..e18cb19244d 100644 --- a/src/locales/es/modifier-type.json +++ b/src/locales/es/modifier-type.json @@ -49,8 +49,8 @@ "DoubleBattleChanceBoosterModifierType": { "description": "Duplica la posibilidad de que un encuentro sea una combate doble durante {{battleCount}} combates." }, - "TempBattleStatBoosterModifierType": { - "description": "Aumenta la est. {{tempBattleStatName}} de todos los miembros del equipo en 1 nivel durante 5 combates." + "TempStatStageBoosterModifierType": { + "description": "Aumenta la est. {{stat}} de todos los miembros del equipo en 1 nivel durante 5 combates." }, "AttackTypeBoosterModifierType": { "description": "Aumenta la potencia de los movimientos de tipo {{moveType}} de un Pokémon en un 20%." @@ -61,8 +61,8 @@ "AllPokemonLevelIncrementModifierType": { "description": "Aumenta el nivel de todos los miembros del equipo en {{levels}}." }, - "PokemonBaseStatBoosterModifierType": { - "description": "Aumenta la est. {{statName}} base del portador en un 10%.\nCuanto mayores sean tus IVs, mayor será el límite de acumulación." + "BaseStatBoosterModifierType": { + "description": "Aumenta la est. {{stat}} base del portador en un 10%.\nCuanto mayores sean tus IVs, mayor será el límite de acumulación." }, "AllPokemonFullHpRestoreModifierType": { "description": "Restaura el 100% de los PS de todos los Pokémon." @@ -248,6 +248,12 @@ "name": "Periscopio", "description": "Aumenta la probabilidad de asestar un golpe crítico." }, + "DIRE_HIT": { + "name": "Crítico X", + "extra": { + "raises": "Critical Hit Ratio" + } + }, "LEEK": { "name": "Puerro", "description": "Puerro muy largo y duro que aumenta la probabilidad de asestar un golpe crítico. Debe llevarlo Farfetch'd." @@ -411,25 +417,13 @@ "description": "Polvo muy fino, pero a la vez poderoso, que aumenta la Velocidad. Debe llevarlo Ditto." } }, - "TempBattleStatBoosterItem": { + "TempStatStageBoosterItem": { "x_attack": "Ataque X", "x_defense": "Defensa X", "x_sp_atk": "Ataq. Esp. X", "x_sp_def": "Def. Esp. X", "x_speed": "Velocidad X", - "x_accuracy": "Precisión X", - "dire_hit": "Crítico X" - }, - "TempBattleStatBoosterStatName": { - "ATK": "Ataque", - "DEF": "Defensa", - "SPATK": "Ataq. Esp.", - "SPDEF": "Def. Esp.", - "SPD": "Velocidad", - "ACC": "Precisión", - "CRIT": "Tasa de crítico", - "EVA": "Evasión", - "DEFAULT": "???" + "x_accuracy": "Precisión X" }, "AttackTypeBoosterItem": { "silk_scarf": "Pañuelo seda", diff --git a/src/locales/es/move-trigger.json b/src/locales/es/move-trigger.json index 52a6f86d930..f92b7950a07 100644 --- a/src/locales/es/move-trigger.json +++ b/src/locales/es/move-trigger.json @@ -1,4 +1,8 @@ { + "switchedTwoStatChanges": "{{pokemonName}} ha intercambiado los cambios en {{firstStat}} y {{secondStat}} con los del objetivo!", + "switchedStat": "{{pokemonName}} cambia su {{stat}} por la de su objetivo!", + "sharedGuard": "{{pokemonName}} suma su capacidad defensiva a la del objetivo y la reparte equitativamente!", + "sharedPower": "{{pokemonName}} suma su capacidad ofensiva a la del objetivo y la reparte equitativamente!", "isChargingPower": "¡{{pokemonName}} está acumulando energía!", "burnedItselfOut": "¡El fuego interior de {{pokemonName}} se ha extinguido!", "startedHeatingUpBeak": "¡{{pokemonName}} empieza\na calentar su pico!", @@ -9,4 +13,4 @@ "statEliminated": "¡Los cambios en estadísticas fueron eliminados!", "revivalBlessing": "¡{{pokemonName}} ha revivido!", "safeguard": "¡{{targetName}} está protegido por Velo Sagrado!" -} \ No newline at end of file +} diff --git a/src/locales/fr/achv.json b/src/locales/fr/achv.json index 60655ae22cf..3e95f9326ca 100644 --- a/src/locales/fr/achv.json +++ b/src/locales/fr/achv.json @@ -92,7 +92,7 @@ "name": "Master Maitre de la Ligue", "name_female": "Master Maitresse de la Ligue" }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { "name": "Travail d’équipe", "description": "Utiliser Relais avec au moins une statistique montée à fond." }, diff --git a/src/locales/fr/modifier-type.json b/src/locales/fr/modifier-type.json index 935deeb5f62..509a8b11112 100644 --- a/src/locales/fr/modifier-type.json +++ b/src/locales/fr/modifier-type.json @@ -49,8 +49,8 @@ "DoubleBattleChanceBoosterModifierType": { "description": "Double les chances de tomber sur un combat double pendant {{battleCount}} combats." }, - "TempBattleStatBoosterModifierType": { - "description": "Augmente d’un cran {{tempBattleStatName}} pour toute l’équipe pendant 5 combats." + "TempStatStageBoosterModifierType": { + "description": "Augmente d’un cran {{stat}} pour toute l’équipe pendant 5 combats." }, "AttackTypeBoosterModifierType": { "description": "Augmente de 20% la puissance des capacités de type {{moveType}} d’un Pokémon." @@ -61,8 +61,8 @@ "AllPokemonLevelIncrementModifierType": { "description": "Fait monter toute l’équipe de {{levels}} niveau·x." }, - "PokemonBaseStatBoosterModifierType": { - "description": "Augmente de 10% {{statName}} de base de son porteur. Plus les IV sont hauts, plus il peut en porter." + "BaseStatBoosterModifierType": { + "description": "Augmente de 10% {{stat}} de base de son porteur. Plus les IV sont hauts, plus il peut en porter." }, "AllPokemonFullHpRestoreModifierType": { "description": "Restaure tous les PV de toute l’équipe." @@ -183,6 +183,7 @@ "SOOTHE_BELL": { "name": "Grelot Zen" }, "SCOPE_LENS": { "name": "Lentilscope", "description": "Une lentille qui augmente d’un cran le taux de critiques du porteur." }, + "DIRE_HIT": { "name": "Muscle +", "extra": { "raises": "Taux de critique" } }, "LEEK": { "name": "Poireau", "description": "À faire tenir à Canarticho ou Palarticho. Un poireau très long et solide qui augmente de 2 crans le taux de critiques." }, "EVIOLITE": { "name": "Évoluroc", "description": "Augmente de 50% la Défense et Déf. Spé. si le porteur peut évoluer, 25% aux fusions dont une moitié le peut encore." }, @@ -250,28 +251,14 @@ "METAL_POWDER": { "name": "Poudre Métal", "description": "À faire tenir à Métamorph. Cette poudre étrange, très fine mais résistante, double sa Défense." }, "QUICK_POWDER": { "name": "Poudre Vite", "description": "À faire tenir à Métamorph. Cette poudre étrange, très fine mais résistante, double sa Vitesse." } }, - "TempBattleStatBoosterItem": { + "TempStatStageBoosterItem": { "x_attack": "Attaque +", "x_defense": "Défense +", "x_sp_atk": "Atq. Spé. +", "x_sp_def": "Déf. Spé. +", "x_speed": "Vitesse +", - "x_accuracy": "Précision +", - "dire_hit": "Muscle +" + "x_accuracy": "Précision +" }, - - "TempBattleStatBoosterStatName": { - "ATK": "l’Attaque", - "DEF": "la Défense", - "SPATK": "l’Atq. Spé.", - "SPDEF": "la Déf. Spé.", - "SPD": "la Vitesse", - "ACC": "la précision", - "CRIT": "le taux de critique", - "EVA": "l’esquive", - "DEFAULT": "???" - }, - "AttackTypeBoosterItem": { "silk_scarf": "Mouchoir Soie", "black_belt": "Ceinture Noire", diff --git a/src/locales/fr/modifier.json b/src/locales/fr/modifier.json index 8a15c9e5ddf..0ec228a22c2 100644 --- a/src/locales/fr/modifier.json +++ b/src/locales/fr/modifier.json @@ -3,7 +3,7 @@ "turnHealApply": "Les PV de {{pokemonNameWithAffix}}\nsont un peu restaurés par les {{typeName}} !", "hitHealApply": "Les PV de {{pokemonNameWithAffix}}\nsont un peu restaurés par le {{typeName}} !", "pokemonInstantReviveApply": "{{pokemonNameWithAffix}} a repris connaissance\navec sa {{typeName}} et est prêt à se battre de nouveau !", - "pokemonResetNegativeStatStageApply": "Les stats baissées de {{pokemonNameWithAffix}}\nsont restaurées par l’{{typeName}} !", + "resetNegativeStatStageApply": "Les stats baissées de {{pokemonNameWithAffix}}\nsont restaurées par l’{{typeName}} !", "moneyInterestApply": "La {{typeName}} vous rapporte\n{{moneyAmount}} ₽ d’intérêts !", "turnHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} est absorbé·e\npar le {{typeName}} de {{pokemonName}} !", "contactHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} est volé·e\npar l’{{typeName}} de {{pokemonName}} !", diff --git a/src/locales/fr/move-trigger.json b/src/locales/fr/move-trigger.json index 5c814745a8e..b9bc929c619 100644 --- a/src/locales/fr/move-trigger.json +++ b/src/locales/fr/move-trigger.json @@ -3,6 +3,10 @@ "cutHpPowerUpMove": "{{pokemonName}} sacrifie des PV\net augmente la puissance ses capacités !", "absorbedElectricity": "{{pokemonName}} absorbe de l’électricité !", "switchedStatChanges": "{{pokemonName}} permute\nles changements de stats avec ceux de sa cible !", + "switchedTwoStatChanges": "{{pokemonName}} permute les changements de {{firstStat} et de {{secondStat}} avec ceux de sa cible !", + "switchedStat": "{{pokemonName}} et sa cible échangent leur {{stat}} !", + "sharedGuard": "{{pokemonName}} additionne sa garde à celle de sa cible et redistribue le tout équitablement !", + "sharedPower": "{{pokemonName}} additionne sa force à celle de sa cible et redistribue le tout équitablement !", "goingAllOutForAttack": "{{pokemonName}} a pris\ncette capacité au sérieux !", "regainedHealth": "{{pokemonName}}\nrécupère des PV !", "keptGoingAndCrashed": "{{pokemonName}}\ns’écrase au sol !", diff --git a/src/locales/it/achv.json b/src/locales/it/achv.json index 98e41005c46..d1607f6c548 100644 --- a/src/locales/it/achv.json +++ b/src/locales/it/achv.json @@ -80,7 +80,7 @@ "100_RIBBONS": { "name": "Campione Lega Assoluta" }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { "name": "Lavoro di Squadra", "description": "Trasferisci almeno sei bonus statistiche tramite staffetta" }, @@ -261,4 +261,4 @@ "name": "Buona la prima!", "description": "Completa la modalità sfida 'Un nuovo inizio'." } -} \ No newline at end of file +} diff --git a/src/locales/it/modifier-type.json b/src/locales/it/modifier-type.json index b466e5bb9a3..99c06bb2038 100644 --- a/src/locales/it/modifier-type.json +++ b/src/locales/it/modifier-type.json @@ -49,8 +49,8 @@ "DoubleBattleChanceBoosterModifierType": { "description": "Raddoppia la possibilità di imbattersi in doppie battaglie per {{battleCount}} battaglie." }, - "TempBattleStatBoosterModifierType": { - "description": "Aumenta {{tempBattleStatName}} di un livello a tutti i Pokémon nel gruppo per 5 battaglie." + "TempStatStageBoosterModifierType": { + "description": "Aumenta la statistica {{stat}} di un livello\na tutti i Pokémon nel gruppo per 5 battaglie." }, "AttackTypeBoosterModifierType": { "description": "Aumenta la potenza delle mosse di tipo {{moveType}} del 20% per un Pokémon." @@ -59,10 +59,10 @@ "description": "Aumenta il livello di un Pokémon di {{levels}}." }, "AllPokemonLevelIncrementModifierType": { - "description": "Aumenta i livell di tutti i Pokémon della squadra di {{levels}}." + "description": "Aumenta il livello di tutti i Pokémon della squadra di {{levels}}." }, - "PokemonBaseStatBoosterModifierType": { - "description": "Aumenta {{statName}} di base del possessore del 10%." + "BaseStatBoosterModifierType": { + "description": "Aumenta l'/la {{stat}} di base del possessore del 10%." }, "AllPokemonFullHpRestoreModifierType": { "description": "Restituisce il 100% dei PS a tutti i Pokémon." @@ -248,6 +248,12 @@ "name": "Mirino", "description": "Lente che aumenta la probabilità di sferrare brutti colpi." }, + "DIRE_HIT": { + "name": "Supercolpo", + "extra": { + "raises": "Tasso di brutti colpi" + } + }, "LEEK": { "name": "Porro", "description": "Strumento da dare a Farfetch'd. Lungo gambo di porro che aumenta la probabilità di sferrare brutti colpi." @@ -411,25 +417,13 @@ "description": "Strumento da dare a Ditto. Questa strana polvere, fine e al contempo dura, aumenta la Velocità." } }, - "TempBattleStatBoosterItem": { + "TempStatStageBoosterItem": { "x_attack": "Attacco X", "x_defense": "Difesa X", "x_sp_atk": "Att. Speciale X", "x_sp_def": "Dif. Speciale X", "x_speed": "Velocità X", - "x_accuracy": "Precisione X", - "dire_hit": "Supercolpo" - }, - "TempBattleStatBoosterStatName": { - "ATK": "Attacco", - "DEF": "Difesa", - "SPATK": "Att. Speciale", - "SPDEF": "Dif. Speciale", - "SPD": "Velocità", - "ACC": "Precisione", - "CRIT": "Tasso di brutti colpi", - "EVA": "Elusione", - "DEFAULT": "???" + "x_accuracy": "Precisione X" }, "AttackTypeBoosterItem": { "silk_scarf": "Sciarpa seta", @@ -606,4 +600,4 @@ "FAIRY_MEMORY": "ROM Folletto", "NORMAL_MEMORY": "ROM Normale" } -} \ No newline at end of file +} diff --git a/src/locales/it/modifier.json b/src/locales/it/modifier.json index 397a1f21f9a..c42bf04bc8a 100644 --- a/src/locales/it/modifier.json +++ b/src/locales/it/modifier.json @@ -3,7 +3,7 @@ "turnHealApply": "{{pokemonNameWithAffix}} recupera alcuni PS con\nil/la suo/a {{typeName}}!", "hitHealApply": "{{pokemonNameWithAffix}} recupera alcuni PS con\nil/la suo/a {{typeName}}!", "pokemonInstantReviveApply": "{{pokemonNameWithAffix}} torna in forze\ngrazie al/alla suo/a {{typeName}}!", - "pokemonResetNegativeStatStageApply": "La riduzione alle statistiche di {{pokemonNameWithAffix}}\nviene annullata grazie al/alla suo/a {{typeName}}!", + "resetNegativeStatStageApply": "La riduzione alle statistiche di {{pokemonNameWithAffix}}\nviene annullata grazie al/alla suo/a {{typeName}}!", "moneyInterestApply": "Ricevi un interesse pari a {{moneyAmount}}₽\ngrazie al/allo/a {{typeName}}!", "turnHeldItemTransferApply": "Il/l'/lo/la {{itemName}} di {{pokemonNameWithAffix}} è stato assorbito\ndal {{typeName}} di {{pokemonName}}!", "contactHeldItemTransferApply": "Il/l'/lo/la {{itemName}} di {{pokemonNameWithAffix}} è stato rubato\nda {{pokemonName}} con {{typeName}}!", diff --git a/src/locales/it/move-trigger.json b/src/locales/it/move-trigger.json index 58b7b1a4c5b..785972b90f9 100644 --- a/src/locales/it/move-trigger.json +++ b/src/locales/it/move-trigger.json @@ -3,6 +3,10 @@ "cutHpPowerUpMove": "{{pokemonName}} riduce i suoi PS per potenziare la sua mossa!", "absorbedElectricity": "{{pokemonName}} assorbe elettricità!", "switchedStatChanges": "{{pokemonName}} scambia con il bersaglio le modifiche alle statistiche!", + "switchedTwoStatChanges": "{{pokemonName}} scambia con il bersaglio le modifiche a {{firstStat}} e {{secondStat}}!", + "switchedStat": "{{pokemonName}} scambia la sua {{stat}} con quella del bersaglio!", + "sharedGuard": "{{pokemonName}} somma le sue capacità difensive con quelle del bersaglio e le ripartisce equamente!", + "sharedPower": "{{pokemonName}} somma le sue capacità offensive con quelle del bersaglio e le ripartisce equamente!", "goingAllOutForAttack": "{{pokemonName}} fa sul serio!", "regainedHealth": "{{pokemonName}} s'è\nripreso!", "keptGoingAndCrashed": "{{pokemonName}} si sbilancia e\nsi schianta!", diff --git a/src/locales/ja/achv.json b/src/locales/ja/achv.json index 809375e5c7e..182da0aed2e 100644 --- a/src/locales/ja/achv.json +++ b/src/locales/ja/achv.json @@ -81,7 +81,7 @@ "100_RIBBONS": { "name": "マスターリーグチャンピオン" }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { "name": "同力", "description": "少なくとも 一つの 能力を 最大まで あげて\n他の 手持ちポケモンに バトンタッチする" }, diff --git a/src/locales/ja/modifier-type.json b/src/locales/ja/modifier-type.json index 6effb1d9d82..f1fcc4d3005 100644 --- a/src/locales/ja/modifier-type.json +++ b/src/locales/ja/modifier-type.json @@ -49,8 +49,8 @@ "DoubleBattleChanceBoosterModifierType": { "description": "バトル{{battleCount}}かいのあいだ ダブルバトルになるかくりつを2ばいにする" }, - "TempBattleStatBoosterModifierType": { - "description": "すべてのパーティメンバーの {{tempBattleStatName}}を5かいのバトルのあいだ 1だんかいあげる" + "TempStatStageBoosterModifierType": { + "description": "すべてのパーティメンバーの {{stat}}を5かいのバトルのあいだ 1だんかいあげる" }, "AttackTypeBoosterModifierType": { "description": "ポケモンの {{moveType}}タイプのわざのいりょくを20パーセントあげる" @@ -61,8 +61,8 @@ "AllPokemonLevelIncrementModifierType": { "description": "すべてのパーティメンバーのレベルを1あげる" }, - "PokemonBaseStatBoosterModifierType": { - "description": "ポケモンの{{statName}}のきほんステータスを10パーセントあげる。こたいちがたかいほどスタックのげんかいもたかくなる。" + "BaseStatBoosterModifierType": { + "description": "ポケモンの{{stat}}のきほんステータスを10パーセントあげる。こたいちがたかいほどスタックのげんかいもたかくなる。" }, "AllPokemonFullHpRestoreModifierType": { "description": "すべてのポケモンのHPを100パーセントかいふくする" @@ -248,6 +248,12 @@ "name": "ピントレンズ", "description": "弱点が 見える レンズ。持たせた ポケモンの技が 急所に 当たりやすくなる。" }, + "DIRE_HIT": { + "name": "クリティカット", + "extra": { + "raises": "きゅうしょりつ" + } + }, "LEEK": { "name": "ながねぎ", "description": "とても長くて 硬いクキ。カモネギに 持たせると 技が 急所に 当たりやすくなる。" @@ -411,25 +417,13 @@ "description": "メタモンに 持たせると 素早さが あがる 不思議 粉。とても こまかくて 硬い。" } }, - "TempBattleStatBoosterItem": { + "TempStatStageBoosterItem": { "x_attack": "プラスパワー", "x_defense": "ディフェンダー", "x_sp_atk": "スペシャルアップ", "x_sp_def": "スペシャルガード", "x_speed": "スピーダー", - "x_accuracy": "ヨクアタール", - "dire_hit": "クリティカット" - }, - "TempBattleStatBoosterStatName": { - "ATK": "こうげき", - "DEF": "ぼうぎょ", - "SPATK": "とくこう", - "SPDEF": "とくぼう", - "SPD": "すばやさ", - "ACC": "めいちゅう", - "CRIT": "きゅうしょりつ", - "EVA": "かいひ", - "DEFAULT": "???" + "x_accuracy": "ヨクアタール" }, "AttackTypeBoosterItem": { "silk_scarf": "シルクのスカーフ", @@ -569,4 +563,4 @@ "DOUSE_DRIVE": "アクアカセット", "ULTRANECROZIUM_Z": "ウルトラネクロZ" } -} \ No newline at end of file +} diff --git a/src/locales/ja/modifier.json b/src/locales/ja/modifier.json index 30d5270d94f..a42a849e232 100644 --- a/src/locales/ja/modifier.json +++ b/src/locales/ja/modifier.json @@ -3,7 +3,7 @@ "turnHealApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 少し 回復!", "hitHealApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 少し 回復!", "pokemonInstantReviveApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 復活した!", - "pokemonResetNegativeStatStageApply": "{{pokemonNameWithAffix}}は {{typeName}}で\n下がった能力が 元に戻った!", + "resetNegativeStatStageApply": "{{pokemonNameWithAffix}}は {{typeName}}で\n下がった能力が 元に戻った!", "moneyInterestApply": "{{typeName}}から {{moneyAmount}}円 取得した!", "turnHeldItemTransferApply": "{{pokemonName}}の {{typeName}}が\n{{pokemonNameWithAffix}}の {{itemName}}を 吸い取った!", "contactHeldItemTransferApply": "{{pokemonName}}の {{typeName}}が\n{{pokemonNameWithAffix}}の {{itemName}}を うばい取った!", diff --git a/src/locales/ja/move-trigger.json b/src/locales/ja/move-trigger.json index 0c404feeed6..11a327c01d7 100644 --- a/src/locales/ja/move-trigger.json +++ b/src/locales/ja/move-trigger.json @@ -2,7 +2,9 @@ "hitWithRecoil": "{{pokemonName}}は\nはんどうによる ダメージを うけた!", "cutHpPowerUpMove": "{{pokemonName}}は\nたいりょくを けずって パワーぜんかい!", "absorbedElectricity": "{{pokemonName}}は\n でんきを きゅうしゅうした!", - "switchedStatChanges": "{{pokemonName}}は あいてと じぶんのn\nのうりょくへんかを いれかえた!", + "switchedStatChanges": "{{pokemonName}}は あいてと じぶんの\nのうりょくへんかを いれかえた!", + "sharedGuard": "{{pokemonName}}は\nおたがいのガードを シェアした!", + "sharedPower": "{{pokemonName}}は\nおたがいのパワーを シェアした!", "goingAllOutForAttack": "{{pokemonName}}は\nほんきを だした!", "regainedHealth": "{{pokemonName}}は\nたいりょくを かいふくした!", "keptGoingAndCrashed": "いきおいあまって {{pokemonName}}は\nじめんに ぶつかった!", @@ -59,4 +61,4 @@ "suppressAbilities": "{{pokemonName}}の とくせいが きかなくなった!", "revivalBlessing": "{{pokemonName}}は\n復活して 戦えるようになった!", "swapArenaTags": "{{pokemonName}}は\nおたがいの ばのこうかを いれかえた!" -} \ No newline at end of file +} diff --git a/src/locales/ko/achv.json b/src/locales/ko/achv.json index b9fd327ef3b..9364c1c55b6 100644 --- a/src/locales/ko/achv.json +++ b/src/locales/ko/achv.json @@ -80,7 +80,7 @@ "100_RIBBONS": { "name": "마스터 리그 챔피언" }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { "name": "팀워크", "description": "한 개 이상의 능력치가 최대 랭크일 때 배턴터치 사용" }, diff --git a/src/locales/ko/modifier-type.json b/src/locales/ko/modifier-type.json index a5b3405b33f..d94837bb0d2 100644 --- a/src/locales/ko/modifier-type.json +++ b/src/locales/ko/modifier-type.json @@ -49,8 +49,8 @@ "DoubleBattleChanceBoosterModifierType": { "description": "{{battleCount}}번의 배틀 동안 더블 배틀이 등장할 확률이 두 배가 된다." }, - "TempBattleStatBoosterModifierType": { - "description": "자신의 모든 포켓몬이 5번의 배틀 동안 {{tempBattleStatName}}[[가]] 한 단계 증가한다." + "TempStatStageBoosterModifierType": { + "description": "자신의 모든 포켓몬이 5번의 배틀 동안 {{stat}}[[가]] 한 단계 증가한다." }, "AttackTypeBoosterModifierType": { "description": "지니게 하면 {{moveType}}타입 기술의 위력이 20% 상승한다." @@ -61,8 +61,8 @@ "AllPokemonLevelIncrementModifierType": { "description": "자신의 모든 포켓몬의 레벨이 {{levels}}만큼 상승한다." }, - "PokemonBaseStatBoosterModifierType": { - "description": "지니게 하면 {{statName}} 종족값을 10% 올려준다. 개체값이 높을수록 더 많이 누적시킬 수 있다." + "BaseStatBoosterModifierType": { + "description": "지니게 하면 {{stat}} 종족값을 10% 올려준다. 개체값이 높을수록 더 많이 누적시킬 수 있다." }, "AllPokemonFullHpRestoreModifierType": { "description": "자신의 포켓몬의 HP를 모두 회복한다." @@ -248,6 +248,12 @@ "name": "초점렌즈", "description": "약점이 보이는 렌즈. 지니게 한 포켓몬의 기술이 급소에 맞기 쉬워진다." }, + "DIRE_HIT": { + "name": "크리티컬커터", + "extra": { + "raises": "급소율" + } + }, "LEEK": { "name": "대파", "description": "매우 길고 단단한 줄기. 파오리에게 지니게 하면 기술이 급소에 맞기 쉬워진다." @@ -411,25 +417,13 @@ "description": "메타몽에게 지니게 하면 스피드가 올라가는 이상한 가루. 매우 잘고 단단하다." } }, - "TempBattleStatBoosterItem": { + "TempStatStageBoosterItem": { "x_attack": "플러스파워", "x_defense": "디펜드업", "x_sp_atk": "스페셜업", "x_sp_def": "스페셜가드", "x_speed": "스피드업", - "x_accuracy": "잘-맞히기", - "dire_hit": "크리티컬커터" - }, - "TempBattleStatBoosterStatName": { - "ATK": "공격", - "DEF": "방어", - "SPATK": "특수공격", - "SPDEF": "특수방어", - "SPD": "스피드", - "ACC": "명중률", - "CRIT": "급소율", - "EVA": "회피율", - "DEFAULT": "???" + "x_accuracy": "잘-맞히기" }, "AttackTypeBoosterItem": { "silk_scarf": "실크스카프", @@ -606,4 +600,4 @@ "FAIRY_MEMORY": "페어리메모리", "NORMAL_MEMORY": "일반메모리" } -} \ No newline at end of file +} diff --git a/src/locales/ko/move-trigger.json b/src/locales/ko/move-trigger.json index f0e0fbd6a56..2a38bb13b0a 100644 --- a/src/locales/ko/move-trigger.json +++ b/src/locales/ko/move-trigger.json @@ -3,6 +3,10 @@ "cutHpPowerUpMove": "{{pokemonName}}[[는]]\n체력을 깎아서 자신의 기술을 강화했다!", "absorbedElectricity": "{{pokemonName}}는(은)\n전기를 흡수했다!", "switchedStatChanges": "{{pokemonName}}[[는]] 상대와 자신의\n능력 변화를 바꿨다!", + "switchedTwoStatChanges": "{{pokemonName}} 상대와 자신의 {{firstStat}}과 {{secondStat}}의 능력 변화를 바꿨다!", + "switchedStat": "{{pokemonName}} 서로의 {{stat}}를 교체했다!", + "sharedGuard": "{{pokemonName}} 서로의 가드를 셰어했다!", + "sharedPower": "{{pokemonName}} 서로의 파워를 셰어했다!", "goingAllOutForAttack": "{{pokemonName}}[[는]]\n전력을 다하기 시작했다!", "regainedHealth": "{{pokemonName}}[[는]]\n기력을 회복했다!", "keptGoingAndCrashed": "{{pokemonName}}[[는]]\n의욕이 넘쳐서 땅에 부딪쳤다!", @@ -63,4 +67,4 @@ "swapArenaTags": "{{pokemonName}}[[는]]\n서로의 필드 효과를 교체했다!", "exposedMove": "{{pokemonName}}[[는]]\n{{targetPokemonName}}의 정체를 꿰뚫어 보았다!", "safeguard": "{{targetName}}[[는]] 신비의 베일이 지켜 주고 있다!" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/achv.json b/src/locales/pt_BR/achv.json index acdec1ae306..93e982b60ea 100644 --- a/src/locales/pt_BR/achv.json +++ b/src/locales/pt_BR/achv.json @@ -84,7 +84,7 @@ "100_RIBBONS": { "name": "Fita de Diamante" }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { "name": "Trabalho em Equipe", "description": "Use Baton Pass com pelo menos um atributo aumentado ao máximo" }, @@ -269,4 +269,4 @@ "name": "A torre da derrotA", "description": "Complete o desafio da Batalha Inversa.\n.asrevnI ahlataB ad oifased o etelpmoC" } -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/modifier-type.json b/src/locales/pt_BR/modifier-type.json index b02281a53b8..823d6b35e16 100644 --- a/src/locales/pt_BR/modifier-type.json +++ b/src/locales/pt_BR/modifier-type.json @@ -49,8 +49,8 @@ "DoubleBattleChanceBoosterModifierType": { "description": "Dobra as chances de encontrar uma batalha em dupla por {{battleCount}} batalhas." }, - "TempBattleStatBoosterModifierType": { - "description": "Aumenta o atributo de {{tempBattleStatName}} para todos os membros da equipe por 5 batalhas." + "TempStatStageBoosterModifierType": { + "description": "Aumenta o atributo de {{stat}} para todos os membros da equipe por 5 batalhas." }, "AttackTypeBoosterModifierType": { "description": "Aumenta o poder dos ataques do tipo {{moveType}} de um Pokémon em 20%." @@ -61,8 +61,8 @@ "AllPokemonLevelIncrementModifierType": { "description": "Aumenta em {{levels}} o nível de todos os membros da equipe." }, - "PokemonBaseStatBoosterModifierType": { - "description": "Aumenta o atributo base de {{statName}} em 10%. Quanto maior os IVs, maior o limite de aumento." + "BaseStatBoosterModifierType": { + "description": "Aumenta o atributo base de {{stat}} em 10%. Quanto maior os IVs, maior o limite de aumento." }, "AllPokemonFullHpRestoreModifierType": { "description": "Restaura totalmente os PS de todos os Pokémon." @@ -248,6 +248,12 @@ "name": "Lentes de Mira", "description": "Estas lentes facilitam o foco em pontos fracos. Aumenta a chance de acerto crítico de quem a segurar." }, + "DIRE_HIT": { + "name": "Direto", + "extra": { + "raises": "Chance de Acerto Crítico" + } + }, "LEEK": { "name": "Alho-poró", "description": "Esse talo de alho-poró muito longo e rígido aumenta a taxa de acerto crítico dos movimentos do Farfetch'd." @@ -411,25 +417,13 @@ "description": "Extremamente fino, porém duro, este pó estranho aumenta o atributo de Velocidade de Ditto." } }, - "TempBattleStatBoosterItem": { + "TempStatStageBoosterItem": { "x_attack": "Ataque X", "x_defense": "Defesa X", "x_sp_atk": "Ataque Esp. X", "x_sp_def": "Defesa Esp. X", "x_speed": "Velocidade X", - "x_accuracy": "Precisão X", - "dire_hit": "Direto" - }, - "TempBattleStatBoosterStatName": { - "ATK": "Ataque", - "DEF": "Defesa", - "SPATK": "Ataque Esp.", - "SPDEF": "Defesa Esp.", - "SPD": "Velocidade", - "ACC": "Precisão", - "CRIT": "Chance de Acerto Crítico", - "EVA": "Evasão", - "DEFAULT": "???" + "x_accuracy": "Precisão X" }, "AttackTypeBoosterItem": { "silk_scarf": "Lenço de Seda", diff --git a/src/locales/pt_BR/modifier.json b/src/locales/pt_BR/modifier.json index 602a0be3a5b..38622de579e 100644 --- a/src/locales/pt_BR/modifier.json +++ b/src/locales/pt_BR/modifier.json @@ -3,7 +3,7 @@ "turnHealApply": "{{pokemonNameWithAffix}} restaurou um pouco de PS usando\nsuas {{typeName}}!", "hitHealApply": "{{pokemonNameWithAffix}} restaurou um pouco de PS usando\nsua {{typeName}}!", "pokemonInstantReviveApply": "{{pokemonNameWithAffix}} foi reanimado\npor sua {{typeName}}!", - "pokemonResetNegativeStatStageApply": "Os atributos diminuídos de {{pokemonNameWithAffix}} foram\nrestaurados por seu(sua) {{typeName}}!", + "resetNegativeStatStageApply": "Os atributos diminuídos de {{pokemonNameWithAffix}} foram\nrestaurados por seu(sua) {{typeName}}!", "moneyInterestApply": "Você recebeu um juros de ₽{{moneyAmount}}\nde sua {{typeName}}!", "turnHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} foi absorvido(a)\npelo {{typeName}} de {{pokemonName}}!", "contactHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} foi pego(a)\npela {{typeName}} de {{pokemonName}}!", diff --git a/src/locales/pt_BR/move-trigger.json b/src/locales/pt_BR/move-trigger.json index ea320412a24..9aa13dedad5 100644 --- a/src/locales/pt_BR/move-trigger.json +++ b/src/locales/pt_BR/move-trigger.json @@ -63,4 +63,4 @@ "swapArenaTags": "{{pokemonName}} trocou os efeitos de batalha que afetam cada lado do campo!", "exposedMove": "{{pokemonName}} identificou\n{{targetPokemonName}}!", "safeguard": "{{targetName}} está protegido por Safeguard!" -} \ No newline at end of file +} diff --git a/src/locales/zh_CN/achv.json b/src/locales/zh_CN/achv.json index 8de0c48a2c3..90dfda0e3c1 100644 --- a/src/locales/zh_CN/achv.json +++ b/src/locales/zh_CN/achv.json @@ -86,7 +86,7 @@ "name": "大师球联盟冠军" }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { "name": "团队协作", "description": "在一项属性强化至最大时用接力棒传递给其他宝可梦" }, diff --git a/src/locales/zh_CN/modifier-type.json b/src/locales/zh_CN/modifier-type.json index 4a982b77cea..5d6184640b1 100644 --- a/src/locales/zh_CN/modifier-type.json +++ b/src/locales/zh_CN/modifier-type.json @@ -49,8 +49,8 @@ "DoubleBattleChanceBoosterModifierType": { "description": "接下来的{{battleCount}}场战斗是双打的概率翻倍。" }, - "TempBattleStatBoosterModifierType": { - "description": "为所有成员宝可梦提升一级{{tempBattleStatName}},持续5场战斗。" + "TempStatStageBoosterModifierType": { + "description": "为所有成员宝可梦提升一级{{stat}},持续5场战斗。" }, "AttackTypeBoosterModifierType": { "description": "一只宝可梦的{{moveType}}系招式威力提升20%。" @@ -61,8 +61,8 @@ "AllPokemonLevelIncrementModifierType": { "description": "使一只寶可夢的等級提升{{levels}}級。" }, - "PokemonBaseStatBoosterModifierType": { - "description": "增加10%持有者的{{statName}},\n个体值越高堆叠上限越高。" + "BaseStatBoosterModifierType": { + "description": "增加10%持有者的{{stat}},\n个体值越高堆叠上限越高。" }, "AllPokemonFullHpRestoreModifierType": { "description": "所有宝可梦完全回复HP。" @@ -248,6 +248,12 @@ "name": "焦点镜", "description": "能看见弱点的镜片。携带它的宝可梦的招式\n会变得容易击中要害。" }, + "DIRE_HIT": { + "name": "要害攻击", + "extra": { + "raises": "会心" + } + }, "LEEK": { "name": "大葱", "description": "非常长且坚硬的茎。让大葱鸭携带后,\n招式会变得容易击中要害。" @@ -411,25 +417,13 @@ "description": "让百变怪携带后,速度就会提高的神奇粉末。\n非常细腻坚硬。" } }, - "TempBattleStatBoosterItem": { + "TempStatStageBoosterItem": { "x_attack": "力量强化", "x_defense": "防御强化", "x_sp_atk": "特攻强化", "x_sp_def": "特防强化", "x_speed": "速度强化", - "x_accuracy": "命中强化", - "dire_hit": "要害攻击" - }, - "TempBattleStatBoosterStatName": { - "ATK": "攻击", - "DEF": "防御", - "SPATK": "特攻", - "SPDEF": "特防", - "SPD": "速度", - "ACC": "命中", - "CRIT": "会心", - "EVA": "闪避", - "DEFAULT": "???" + "x_accuracy": "命中强化" }, "AttackTypeBoosterItem": { "silk_scarf": "丝绸围巾", @@ -606,4 +600,4 @@ "FAIRY_MEMORY": "妖精存储碟", "NORMAL_MEMORY": "一般存储碟" } -} \ No newline at end of file +} diff --git a/src/locales/zh_CN/modifier.json b/src/locales/zh_CN/modifier.json index 707fab20ecc..a50cdd35bc1 100644 --- a/src/locales/zh_CN/modifier.json +++ b/src/locales/zh_CN/modifier.json @@ -3,7 +3,7 @@ "turnHealApply": "{{pokemonNameWithAffix}}用{{typeName}}\n回复了体力!", "hitHealApply": "{{pokemonNameWithAffix}}用{{typeName}}\n回复了体力!", "pokemonInstantReviveApply": "{{pokemonNameWithAffix}}用{{typeName}}\n恢复了活力!", - "pokemonResetNegativeStatStageApply": "{{pokemonNameWithAffix}}降低的能力被{{typeName}}\n复原了!", + "resetNegativeStatStageApply": "{{pokemonNameWithAffix}}降低的能力被{{typeName}}\n复原了!", "moneyInterestApply": "用{{typeName}}\n获得了 ₽{{moneyAmount}} 利息!", "turnHeldItemTransferApply": "{{pokemonNameWithAffix}}的{{itemName}}被\n{{pokemonName}}的{{typeName}}吸收了!", "contactHeldItemTransferApply": "{{pokemonNameWithAffix}}的{{itemName}}被\n{{pokemonName}}的{{typeName}}夺取了!", diff --git a/src/locales/zh_CN/move-trigger.json b/src/locales/zh_CN/move-trigger.json index 44705d54e76..1eb4c397f45 100644 --- a/src/locales/zh_CN/move-trigger.json +++ b/src/locales/zh_CN/move-trigger.json @@ -3,6 +3,10 @@ "cutHpPowerUpMove": "{{pokemonName}}\n削减了体力并提升了招式威力!", "absorbedElectricity": "{{pokemonName}}\n吸收了电力!", "switchedStatChanges": "{{pokemonName}}和对手互换了\n自己的能力变化!", + "switchedTwoStatChanges": "{{pokemonName}} 和对手互换了自己的{{firstStat}}和{{secondStat}}的能力变化!", + "switchedStat": "{{pokemonName}} 互换了各自的{{stat}}!", + "sharedGuard": "{{pokemonName}} 平分了各自的防守!", + "sharedPower": "{{pokemonName}} 平分了各自的力量!", "goingAllOutForAttack": "{{pokemonName}}拿出全力了!", "regainedHealth": "{{pokemonName}}的\n体力回复了!", "keptGoingAndCrashed": "{{pokemonName}}因势头过猛\n而撞到了地面!", diff --git a/src/locales/zh_CN/pokemon-info.json b/src/locales/zh_CN/pokemon-info.json index 5194189c806..a21a8156e4c 100644 --- a/src/locales/zh_CN/pokemon-info.json +++ b/src/locales/zh_CN/pokemon-info.json @@ -1,7 +1,7 @@ { "Stat": { "HP": "最大HP", - "HPshortened": "最大HP", + "HPshortened": "HP", "ATK": "攻击", "ATKshortened": "攻击", "DEF": "防御", @@ -37,4 +37,4 @@ "FAIRY": "妖精", "STELLAR": "星晶" } -} \ No newline at end of file +} diff --git a/src/locales/zh_TW/achv.json b/src/locales/zh_TW/achv.json index 6587394cf41..9edce2e368d 100644 --- a/src/locales/zh_TW/achv.json +++ b/src/locales/zh_TW/achv.json @@ -80,7 +80,7 @@ "100_RIBBONS": { "name": "大師球聯盟冠軍" }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { "name": "團隊協作", "description": "在一項屬性強化至最大時用接力棒傳遞給其他寶可夢" }, @@ -257,4 +257,4 @@ "name": "鏡子子鏡", "description": "完成逆轉之戰挑戰\n戰挑戰之轉逆成完" } -} \ No newline at end of file +} diff --git a/src/locales/zh_TW/modifier-type.json b/src/locales/zh_TW/modifier-type.json index 847ede7001e..68881a206cb 100644 --- a/src/locales/zh_TW/modifier-type.json +++ b/src/locales/zh_TW/modifier-type.json @@ -49,8 +49,8 @@ "DoubleBattleChanceBoosterModifierType": { "description": "接下來的{{battleCount}}場戰鬥是雙打的概率翻倍。" }, - "TempBattleStatBoosterModifierType": { - "description": "爲所有成員寶可夢提升一級{{tempBattleStatName}},持續5場戰鬥。" + "TempStatStageBoosterModifierType": { + "description": "爲所有成員寶可夢提升一級{{stat}},持續5場戰鬥。" }, "AttackTypeBoosterModifierType": { "description": "一隻寶可夢的{{moveType}}系招式威力提升20%。" @@ -61,8 +61,8 @@ "AllPokemonLevelIncrementModifierType": { "description": "Increases all party members' level by {{levels}}." }, - "PokemonBaseStatBoosterModifierType": { - "description": "增加持有者的{{statName}}10%,個體值越高堆疊\n上限越高。" + "BaseStatBoosterModifierType": { + "description": "增加持有者的{{stat}}10%,個體值越高堆疊\n上限越高。" }, "AllPokemonFullHpRestoreModifierType": { "description": "所有寶可夢完全恢復HP。" @@ -244,6 +244,12 @@ "name": "焦點鏡", "description": "能看見弱點的鏡片。攜帶它的寶可夢的招式 會變得容易擊中要害。" }, + "DIRE_HIT": { + "name": "要害攻擊", + "extra": { + "raises": "會心" + } + }, "LEEK": { "name": "大蔥", "description": "非常長且堅硬的莖。讓大蔥鴨攜帶後,招式會 變得容易擊中要害。" @@ -407,25 +413,13 @@ "description": "讓百變怪攜帶後,速度就會提高的神奇粉末。非常細緻堅硬。" } }, - "TempBattleStatBoosterItem": { + "TempStatStageBoosterItem": { "x_attack": "力量強化", "x_defense": "防禦強化", "x_sp_atk": "特攻強化", "x_sp_def": "特防強化", "x_speed": "速度強化", - "x_accuracy": "命中強化", - "dire_hit": "要害攻擊" - }, - "TempBattleStatBoosterStatName": { - "ATK": "攻擊", - "DEF": "防禦", - "SPATK": "特攻", - "SPDEF": "特防", - "SPD": "速度", - "ACC": "命中", - "CRIT": "會心", - "EVA": "閃避", - "DEFAULT": "???" + "x_accuracy": "命中強化" }, "AttackTypeBoosterItem": { "silk_scarf": "絲綢圍巾", @@ -602,4 +596,4 @@ "FAIRY_MEMORY": "妖精記憶碟", "NORMAL_MEMORY": "一般記憶碟" } -} \ No newline at end of file +} diff --git a/src/locales/zh_TW/modifier.json b/src/locales/zh_TW/modifier.json index eb4b5107cff..1c0d4760e6f 100644 --- a/src/locales/zh_TW/modifier.json +++ b/src/locales/zh_TW/modifier.json @@ -8,4 +8,4 @@ "contactHeldItemTransferApply": "{{pokemonNameWithAffix}}的{{itemName}}被\n{{pokemonName}}的{{typeName}}奪取了!", "enemyTurnHealApply": "{{pokemonNameWithAffix}}\n回復了一些體力!", "bypassSpeedChanceApply": "{{pokemonName}}用了{{itemName}}後,行動變快了!" -} \ No newline at end of file +} diff --git a/src/locales/zh_TW/move-trigger.json b/src/locales/zh_TW/move-trigger.json index 60dcc1eab7a..d6d0ce659ea 100644 --- a/src/locales/zh_TW/move-trigger.json +++ b/src/locales/zh_TW/move-trigger.json @@ -3,6 +3,10 @@ "cutHpPowerUpMove": "{{pokemonName}}\n削減體力並提升了招式威力!", "absorbedElectricity": "{{pokemonName}}\n吸收了电力!", "switchedStatChanges": "{{pokemonName}}和對手互換了\n自身的能力變化!", + "switchedTwoStatChanges": "{{pokemonName}} 和對手互換了自身的{{firstStat}}和{{secondStat}}的能力變化!", + "switchedStat": "{{pokemonName}} 互換了各自的{{stat}}!", + "sharedGuard": "{{pokemonName}} 平分了各自的防守!", + "sharedPower": "{{pokemonName}} 平分了各自的力量!", "goingAllOutForAttack": "{{pokemonName}}拿出全力了!", "regainedHealth": "{{pokemonName}}的\n體力回復了!", "keptGoingAndCrashed": "{{pokemonName}}因勢頭過猛\n而撞到了地面!", diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index d7937692a8d..fe586074c79 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -3,12 +3,10 @@ import { AttackMove, allMoves, selfStatLowerMoves } from "../data/move"; import { MAX_PER_TYPE_POKEBALLS, PokeballType, getPokeballCatchMultiplier, getPokeballName } from "../data/pokeball"; import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "../field/pokemon"; import { EvolutionItem, pokemonEvolutions } from "../data/pokemon-evolutions"; -import { Stat, getStatName } from "../data/pokemon-stat"; import { tmPoolTiers, tmSpecies } from "../data/tms"; import { Type } from "../data/type"; import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "../ui/party-ui-handler"; import * as Utils from "../utils"; -import { TempBattleStat, getTempBattleStatBoosterItemName, getTempBattleStatName } from "../data/temp-battle-stat"; import { getBerryEffectDescription, getBerryName } from "../data/berry"; import { Unlockables } from "../system/unlockables"; import { StatusEffect, getStatusEffectDescriptor } from "../data/status-effect"; @@ -28,6 +26,7 @@ import { BerryType } from "#enums/berry-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { getPokemonNameWithAffix } from "#app/messages.js"; +import { PermanentStat, TEMP_BATTLE_STATS, TempBattleStat, Stat, getStatKey } from "#app/enums/stat"; const outputModifierData = false; const useMaxWeightForOutput = false; @@ -447,26 +446,28 @@ export class DoubleBattleChanceBoosterModifierType extends ModifierType { } } -export class TempBattleStatBoosterModifierType extends ModifierType implements GeneratedPersistentModifierType { - public tempBattleStat: TempBattleStat; +export class TempStatStageBoosterModifierType extends ModifierType implements GeneratedPersistentModifierType { + private stat: TempBattleStat; + private key: string; - constructor(tempBattleStat: TempBattleStat) { - super("", getTempBattleStatBoosterItemName(tempBattleStat).replace(/\./g, "").replace(/[ ]/g, "_").toLowerCase(), - (_type, _args) => new Modifiers.TempBattleStatBoosterModifier(this, this.tempBattleStat)); + constructor(stat: TempBattleStat) { + const key = TempStatStageBoosterModifierTypeGenerator.items[stat]; + super("", key, (_type, _args) => new Modifiers.TempStatStageBoosterModifier(this, this.stat)); - this.tempBattleStat = tempBattleStat; + this.stat = stat; + this.key = key; } get name(): string { - return i18next.t(`modifierType:TempBattleStatBoosterItem.${getTempBattleStatBoosterItemName(this.tempBattleStat).replace(/\./g, "").replace(/[ ]/g, "_").toLowerCase()}`); + return i18next.t(`modifierType:TempStatStageBoosterItem.${this.key}`); } - getDescription(scene: BattleScene): string { - return i18next.t("modifierType:ModifierType.TempBattleStatBoosterModifierType.description", { tempBattleStatName: getTempBattleStatName(this.tempBattleStat) }); + getDescription(_scene: BattleScene): string { + return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { stat: i18next.t(getStatKey(this.stat)) }); } getPregenArgs(): any[] { - return [ this.tempBattleStat ]; + return [ this.stat ]; } } @@ -611,40 +612,24 @@ export class AllPokemonLevelIncrementModifierType extends ModifierType { } } -function getBaseStatBoosterItemName(stat: Stat) { - switch (stat) { - case Stat.HP: - return "HP Up"; - case Stat.ATK: - return "Protein"; - case Stat.DEF: - return "Iron"; - case Stat.SPATK: - return "Calcium"; - case Stat.SPDEF: - return "Zinc"; - case Stat.SPD: - return "Carbos"; - } -} +export class BaseStatBoosterModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType { + private stat: PermanentStat; + private key: string; -export class PokemonBaseStatBoosterModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType { - private localeName: string; - private stat: Stat; + constructor(stat: PermanentStat) { + const key = BaseStatBoosterModifierTypeGenerator.items[stat]; + super("", key, (_type, args) => new Modifiers.BaseStatModifier(this, (args[0] as Pokemon).id, this.stat)); - constructor(localeName: string, stat: Stat) { - super("", localeName.replace(/[ \-]/g, "_").toLowerCase(), (_type, args) => new Modifiers.PokemonBaseStatModifier(this, (args[0] as Pokemon).id, this.stat)); - - this.localeName = localeName; this.stat = stat; + this.key = key; } get name(): string { - return i18next.t(`modifierType:BaseStatBoosterItem.${this.localeName.replace(/[ \-]/g, "_").toLowerCase()}`); + return i18next.t(`modifierType:BaseStatBoosterItem.${this.key}`); } - getDescription(scene: BattleScene): string { - return i18next.t("modifierType:ModifierType.PokemonBaseStatBoosterModifierType.description", { statName: getStatName(this.stat) }); + getDescription(_scene: BattleScene): string { + return i18next.t("modifierType:ModifierType.BaseStatBoosterModifierType.description", { stat: i18next.t(getStatKey(this.stat)) }); } getPregenArgs(): any[] { @@ -922,6 +907,48 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator { } } +class BaseStatBoosterModifierTypeGenerator extends ModifierTypeGenerator { + public static readonly items: Record = { + [Stat.HP]: "hp_up", + [Stat.ATK]: "protein", + [Stat.DEF]: "iron", + [Stat.SPATK]: "calcium", + [Stat.SPDEF]: "zinc", + [Stat.SPD]: "carbos" + }; + + constructor() { + super((_party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs) { + return new BaseStatBoosterModifierType(pregenArgs[0]); + } + const randStat: PermanentStat = Utils.randSeedInt(Stat.SPD + 1); + return new BaseStatBoosterModifierType(randStat); + }); + } +} + +class TempStatStageBoosterModifierTypeGenerator extends ModifierTypeGenerator { + public static readonly items: Record = { + [Stat.ATK]: "x_attack", + [Stat.DEF]: "x_defense", + [Stat.SPATK]: "x_sp_atk", + [Stat.SPDEF]: "x_sp_def", + [Stat.SPD]: "x_speed", + [Stat.ACC]: "x_accuracy" + }; + + constructor() { + super((_party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs && (pregenArgs.length === 1) && TEMP_BATTLE_STATS.includes(pregenArgs[0])) { + return new TempStatStageBoosterModifierType(pregenArgs[0]); + } + const randStat: TempBattleStat = Utils.randSeedInt(Stat.ACC, Stat.ATK); + return new TempStatStageBoosterModifierType(randStat); + }); + } +} + /** * Modifier type generator for {@linkcode SpeciesStatBoosterModifierType}, which * encapsulates the logic for weighting the most useful held item from @@ -930,7 +957,7 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator { */ class SpeciesStatBoosterModifierTypeGenerator extends ModifierTypeGenerator { /** Object comprised of the currently available species-based stat boosting held items */ - public static items = { + public static readonly items = { LIGHT_BALL: { stats: [Stat.ATK, Stat.SPATK], multiplier: 2, species: [Species.PIKACHU] }, THICK_CLUB: { stats: [Stat.ATK], multiplier: 2, species: [Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK] }, METAL_POWDER: { stats: [Stat.DEF], multiplier: 2, species: [Species.DITTO] }, @@ -1233,7 +1260,7 @@ export type GeneratorModifierOverride = { type?: SpeciesStatBoosterItem; } | { - name: keyof Pick; + name: keyof Pick; type?: TempBattleStat; } | { @@ -1306,7 +1333,7 @@ export const modifierTypes = { SACRED_ASH: () => new AllPokemonFullReviveModifierType("modifierType:ModifierType.SACRED_ASH", "sacred_ash"), REVIVER_SEED: () => new PokemonHeldItemModifierType("modifierType:ModifierType.REVIVER_SEED", "reviver_seed", (type, args) => new Modifiers.PokemonInstantReviveModifier(type, (args[0] as Pokemon).id)), - WHITE_HERB: () => new PokemonHeldItemModifierType("modifierType:ModifierType.WHITE_HERB", "white_herb", (type, args) => new Modifiers.PokemonResetNegativeStatStageModifier(type, (args[0] as Pokemon).id)), + WHITE_HERB: () => new PokemonHeldItemModifierType("modifierType:ModifierType.WHITE_HERB", "white_herb", (type, args) => new Modifiers.ResetNegativeStatStageModifier(type, (args[0] as Pokemon).id)), ETHER: () => new PokemonPpRestoreModifierType("modifierType:ModifierType.ETHER", "ether", 10), MAX_ETHER: () => new PokemonPpRestoreModifierType("modifierType:ModifierType.MAX_ETHER", "max_ether", -1), @@ -1327,23 +1354,15 @@ export const modifierTypes = { SPECIES_STAT_BOOSTER: () => new SpeciesStatBoosterModifierTypeGenerator(), - TEMP_STAT_BOOSTER: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { - if (pregenArgs && (pregenArgs.length === 1) && (pregenArgs[0] in TempBattleStat)) { - return new TempBattleStatBoosterModifierType(pregenArgs[0] as TempBattleStat); - } - const randTempBattleStat = Utils.randSeedInt(6) as TempBattleStat; - return new TempBattleStatBoosterModifierType(randTempBattleStat); - }), - DIRE_HIT: () => new TempBattleStatBoosterModifierType(TempBattleStat.CRIT), + TEMP_STAT_STAGE_BOOSTER: () => new TempStatStageBoosterModifierTypeGenerator(), - BASE_STAT_BOOSTER: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { - if (pregenArgs && (pregenArgs.length === 1) && (pregenArgs[0] in Stat)) { - const stat = pregenArgs[0] as Stat; - return new PokemonBaseStatBoosterModifierType(getBaseStatBoosterItemName(stat), stat); + DIRE_HIT: () => new class extends ModifierType { + getDescription(_scene: BattleScene): string { + return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { stat: i18next.t("modifierType:ModifierType.DIRE_HIT.extra.raises") }); } - const randStat = Utils.randSeedInt(6) as Stat; - return new PokemonBaseStatBoosterModifierType(getBaseStatBoosterItemName(randStat), randStat); - }), + }("modifierType:ModifierType.DIRE_HIT", "dire_hit", (type, _args) => new Modifiers.TempCritBoosterModifier(type)), + + BASE_STAT_BOOSTER: () => new BaseStatBoosterModifierTypeGenerator(), ATTACK_TYPE_BOOSTER: () => new AttackTypeBoosterModifierTypeGenerator(), @@ -1513,7 +1532,7 @@ const modifierPool: ModifierPool = { return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.LURE, skipInLastClassicWaveOrDefault(2)), - new WeightedModifierType(modifierTypes.TEMP_STAT_BOOSTER, 4), + new WeightedModifierType(modifierTypes.TEMP_STAT_STAGE_BOOSTER, 4), new WeightedModifierType(modifierTypes.BERRY, 2), new WeightedModifierType(modifierTypes.TM_COMMON, 2), ].map(m => { @@ -1626,7 +1645,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.WHITE_HERB, (party: Pokemon[]) => { const checkedAbilities = [Abilities.WEAK_ARMOR, Abilities.CONTRARY, Abilities.MOODY, Abilities.ANGER_SHELL, Abilities.COMPETITIVE, Abilities.DEFIANT]; const weightMultiplier = party.filter( - p => !p.getHeldItems().some(i => i instanceof Modifiers.PokemonResetNegativeStatStageModifier && i.stackCount >= i.getMaxHeldItemCount(p)) && + p => !p.getHeldItems().some(i => i instanceof Modifiers.ResetNegativeStatStageModifier && i.stackCount >= i.getMaxHeldItemCount(p)) && (checkedAbilities.some(a => p.hasAbility(a, false, true)) || p.getMoveset(true).some(m => m && selfStatLowerMoves.includes(m.moveId)))).length; // If a party member has one of the above moves or abilities and doesn't have max herbs, the herb will appear more frequently return 0 * (weightMultiplier ? 2 : 1) + (weightMultiplier ? weightMultiplier * 0 : 0); diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index c1defc4abfd..84d8a1385af 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -3,14 +3,12 @@ import BattleScene from "../battle-scene"; import { getLevelTotalExp } from "../data/exp"; import { MAX_PER_TYPE_POKEBALLS, PokeballType } from "../data/pokeball"; import Pokemon, { PlayerPokemon } from "../field/pokemon"; -import { Stat } from "../data/pokemon-stat"; import { addTextObject, TextStyle } from "../ui/text"; import { Type } from "../data/type"; import { EvolutionPhase } from "../phases/evolution-phase"; import { FusionSpeciesFormEvolution, pokemonEvolutions, pokemonPrevolutions } from "../data/pokemon-evolutions"; import { getPokemonNameWithAffix } from "../messages"; import * as Utils from "../utils"; -import { TempBattleStat } from "../data/temp-battle-stat"; import { getBerryEffectFunc, getBerryPredicate } from "../data/berry"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; @@ -23,6 +21,7 @@ import Overrides from "#app/overrides"; import { ModifierType, modifierTypes } from "./modifier-type"; import { Command } from "#app/ui/command-ui-handler"; import { Species } from "#enums/species"; +import { Stat, type PermanentStat, type TempBattleStat, BATTLE_STATS, TEMP_BATTLE_STATS } from "#app/enums/stat"; import i18next from "i18next"; import { allMoves } from "#app/data/move"; @@ -362,41 +361,160 @@ export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier } } -export class TempBattleStatBoosterModifier extends LapsingPersistentModifier { - private tempBattleStat: TempBattleStat; +/** + * Modifier used for party-wide items, specifically the X items, that + * temporarily increases the stat stage multiplier of the corresponding + * {@linkcode TempBattleStat}. + * @extends LapsingPersistentModifier + * @see {@linkcode apply} + */ +export class TempStatStageBoosterModifier extends LapsingPersistentModifier { + private stat: TempBattleStat; + private multiplierBoost: number; - constructor(type: ModifierTypes.TempBattleStatBoosterModifierType, tempBattleStat: TempBattleStat, battlesLeft?: integer, stackCount?: integer) { - super(type, battlesLeft || 5, stackCount); + constructor(type: ModifierType, stat: TempBattleStat, battlesLeft?: number, stackCount?: number) { + super(type, battlesLeft ?? 5, stackCount); - this.tempBattleStat = tempBattleStat; + this.stat = stat; + // Note that, because we want X Accuracy to maintain its original behavior, + // it will increment as it did previously, directly to the stat stage. + this.multiplierBoost = stat !== Stat.ACC ? 0.3 : 1; } match(modifier: Modifier): boolean { - if (modifier instanceof TempBattleStatBoosterModifier) { - return (modifier as TempBattleStatBoosterModifier).tempBattleStat === this.tempBattleStat - && (modifier as TempBattleStatBoosterModifier).battlesLeft === this.battlesLeft; + if (modifier instanceof TempStatStageBoosterModifier) { + const modifierInstance = modifier as TempStatStageBoosterModifier; + return (modifierInstance.stat === this.stat); } return false; } - clone(): TempBattleStatBoosterModifier { - return new TempBattleStatBoosterModifier(this.type as ModifierTypes.TempBattleStatBoosterModifierType, this.tempBattleStat, this.battlesLeft, this.stackCount); + clone() { + return new TempStatStageBoosterModifier(this.type, this.stat, this.battlesLeft, this.stackCount); } getArgs(): any[] { - return [ this.tempBattleStat, this.battlesLeft ]; + return [ this.stat, this.battlesLeft ]; } - apply(args: any[]): boolean { - const tempBattleStat = args[0] as TempBattleStat; + /** + * Checks if {@linkcode args} contains the necessary elements and if the + * incoming stat is matches {@linkcode stat}. + * @param args [0] {@linkcode TempBattleStat} being checked at the time + * [1] {@linkcode Utils.NumberHolder} N/A + * @returns true if the modifier can be applied, false otherwise + */ + shouldApply(args: any[]): boolean { + return args && (args.length === 2) && TEMP_BATTLE_STATS.includes(args[0]) && (args[0] === this.stat) && (args[1] instanceof Utils.NumberHolder); + } - if (tempBattleStat === this.tempBattleStat) { - const statLevel = args[1] as Utils.IntegerHolder; - statLevel.value = Math.min(statLevel.value + 1, 6); - return true; + /** + * Increases the incoming stat stage matching {@linkcode stat} by {@linkcode multiplierBoost}. + * @param args [0] {@linkcode TempBattleStat} N/A + * [1] {@linkcode Utils.NumberHolder} that holds the resulting value of the stat stage multiplier + */ + apply(args: any[]): boolean { + (args[1] as Utils.NumberHolder).value += this.multiplierBoost; + return true; + } + + /** + * Goes through existing modifiers for any that match the selected modifier, + * which will then either add it to the existing modifiers if none were found + * or, if one was found, it will refresh {@linkcode battlesLeft}. + * @param modifiers {@linkcode PersistentModifier} array of the player's modifiers + * @param _virtual N/A + * @param _scene N/A + * @returns true if the modifier was successfully added or applied, false otherwise + */ + add(modifiers: PersistentModifier[], _virtual: boolean, _scene: BattleScene): boolean { + for (const modifier of modifiers) { + if (this.match(modifier)) { + const modifierInstance = modifier as TempStatStageBoosterModifier; + if (modifierInstance.getBattlesLeft() < 5) { + modifierInstance.battlesLeft = 5; + return true; + } + // should never get here + return false; + } } - return false; + modifiers.push(this); + return true; + } + + getMaxStackCount(_scene: BattleScene, _forThreshold?: boolean): number { + return 1; + } +} + +/** + * Modifier used for party-wide items, namely Dire Hit, that + * temporarily increments the critical-hit stage + * @extends LapsingPersistentModifier + * @see {@linkcode apply} + */ +export class TempCritBoosterModifier extends LapsingPersistentModifier { + constructor(type: ModifierType, battlesLeft?: integer, stackCount?: number) { + super(type, battlesLeft || 5, stackCount); + } + + clone() { + return new TempCritBoosterModifier(this.type, this.stackCount); + } + + match(modifier: Modifier): boolean { + return (modifier instanceof TempCritBoosterModifier); + } + + /** + * Checks if {@linkcode args} contains the necessary elements. + * @param args [1] {@linkcode Utils.NumberHolder} N/A + * @returns true if the critical-hit stage boost applies successfully + */ + shouldApply(args: any[]): boolean { + return args && (args.length === 1) && (args[0] instanceof Utils.NumberHolder); + } + + /** + * Increases the current critical-hit stage value by 1. + * @param args [0] {@linkcode Utils.IntegerHolder} that holds the resulting critical-hit level + * @returns true if the critical-hit stage boost applies successfully + */ + apply(args: any[]): boolean { + (args[0] as Utils.NumberHolder).value++; + return true; + } + + /** + * Goes through existing modifiers for any that match the selected modifier, + * which will then either add it to the existing modifiers if none were found + * or, if one was found, it will refresh {@linkcode battlesLeft}. + * @param modifiers {@linkcode PersistentModifier} array of the player's modifiers + * @param _virtual N/A + * @param _scene N/A + * @returns true if the modifier was successfully added or applied, false otherwise + */ + add(modifiers: PersistentModifier[], _virtual: boolean, _scene: BattleScene): boolean { + for (const modifier of modifiers) { + if (this.match(modifier)) { + const modifierInstance = modifier as TempCritBoosterModifier; + if (modifierInstance.getBattlesLeft() < 5) { + modifierInstance.battlesLeft = 5; + return true; + } + // should never get here + return false; + } + } + + modifiers.push(this); + return true; + } + + getMaxStackCount(_scene: BattleScene, _forThreshold?: boolean): number { + return 1; } } @@ -663,24 +781,30 @@ export class TerastallizeModifier extends LapsingPokemonHeldItemModifier { } } -export class PokemonBaseStatModifier extends PokemonHeldItemModifier { - protected stat: Stat; +/** + * Modifier used for held items, specifically vitamins like Carbos, Hp Up, etc., that + * increase the value of a given {@linkcode PermanentStat}. + * @extends LapsingPersistentModifier + * @see {@linkcode apply} + */ +export class BaseStatModifier extends PokemonHeldItemModifier { + protected stat: PermanentStat; readonly isTransferrable: boolean = false; - constructor(type: ModifierTypes.PokemonBaseStatBoosterModifierType, pokemonId: integer, stat: Stat, stackCount?: integer) { + constructor(type: ModifierType, pokemonId: integer, stat: PermanentStat, stackCount?: integer) { super(type, pokemonId, stackCount); this.stat = stat; } matchType(modifier: Modifier): boolean { - if (modifier instanceof PokemonBaseStatModifier) { - return (modifier as PokemonBaseStatModifier).stat === this.stat; + if (modifier instanceof BaseStatModifier) { + return (modifier as BaseStatModifier).stat === this.stat; } return false; } clone(): PersistentModifier { - return new PokemonBaseStatModifier(this.type as ModifierTypes.PokemonBaseStatBoosterModifierType, this.pokemonId, this.stat, this.stackCount); + return new BaseStatModifier(this.type, this.pokemonId, this.stat, this.stackCount); } getArgs(): any[] { @@ -688,12 +812,12 @@ export class PokemonBaseStatModifier extends PokemonHeldItemModifier { } shouldApply(args: any[]): boolean { - return super.shouldApply(args) && args.length === 2 && args[1] instanceof Array; + return super.shouldApply(args) && args.length === 2 && Array.isArray(args[1]); } apply(args: any[]): boolean { - args[1][this.stat] = Math.min(Math.floor(args[1][this.stat] * (1 + this.getStackCount() * 0.1)), 999999); - + const baseStats = args[1] as number[]; + baseStats[this.stat] = Math.floor(baseStats[this.stat] * (1 + this.getStackCount() * 0.1)); return true; } @@ -1398,42 +1522,48 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier { } /** - * Modifier used for White Herb, which resets negative {@linkcode Stat} changes + * Modifier used for held items, namely White Herb, that restore adverse stat + * stages in battle. * @extends PokemonHeldItemModifier * @see {@linkcode apply} */ -export class PokemonResetNegativeStatStageModifier extends PokemonHeldItemModifier { +export class ResetNegativeStatStageModifier extends PokemonHeldItemModifier { constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) { super(type, pokemonId, stackCount); } matchType(modifier: Modifier) { - return modifier instanceof PokemonResetNegativeStatStageModifier; + return modifier instanceof ResetNegativeStatStageModifier; } clone() { - return new PokemonResetNegativeStatStageModifier(this.type, this.pokemonId, this.stackCount); + return new ResetNegativeStatStageModifier(this.type, this.pokemonId, this.stackCount); } /** - * Restores any negative stat stages of the mon to 0 - * @param args args[0] is the {@linkcode Pokemon} whose stat stages are being checked - * @returns true if any stat changes were applied (item was used), false otherwise + * Goes through the holder's stat stages and, if any are negative, resets that + * stat stage back to 0. + * @param args [0] {@linkcode Pokemon} that holds the held item + * @returns true if any stat stages were reset, false otherwise */ apply(args: any[]): boolean { const pokemon = args[0] as Pokemon; - const loweredStats = pokemon.summonData.battleStats.filter(s => s < 0); - if (loweredStats.length) { - for (let s = 0; s < pokemon.summonData.battleStats.length; s++) { - pokemon.summonData.battleStats[s] = Math.max(0, pokemon.summonData.battleStats[s]); + let statRestored = false; + + for (const s of BATTLE_STATS) { + if (pokemon.getStatStage(s) < 0) { + pokemon.setStatStage(s, 0); + statRestored = true; } - pokemon.scene.queueMessage(i18next.t("modifier:pokemonResetNegativeStatStageApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name })); - return true; } - return false; + + if (statRestored) { + pokemon.scene.queueMessage(i18next.t("modifier:resetNegativeStatStageApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name })); + } + return statRestored; } - getMaxHeldItemCount(pokemon: Pokemon): integer { + getMaxHeldItemCount(_pokemon: Pokemon): integer { return 2; } } @@ -2745,7 +2875,7 @@ export class EnemyFusionChanceModifier extends EnemyPersistentModifier { * - The player * - The enemy * @param scene current {@linkcode BattleScene} - * @param isPlayer {@linkcode boolean} for whether the the player (`true`) or enemy (`false`) is being overridden + * @param isPlayer {@linkcode boolean} for whether the player (`true`) or enemy (`false`) is being overridden */ export function overrideModifiers(scene: BattleScene, isPlayer: boolean = true): void { const modifiersOverride: ModifierTypes.ModifierOverride[] = isPlayer ? Overrides.STARTING_MODIFIER_OVERRIDE : Overrides.OPP_MODIFIER_OVERRIDE; @@ -2760,13 +2890,22 @@ export function overrideModifiers(scene: BattleScene, isPlayer: boolean = true): modifiersOverride.forEach(item => { const modifierFunc = modifierTypes[item.name]; - const modifier = modifierFunc().withIdFromFunc(modifierFunc).newModifier() as PersistentModifier; - modifier.stackCount = item.count || 1; + let modifierType: ModifierType | null = modifierFunc(); - if (isPlayer) { - scene.addModifier(modifier, true, false, false, true); - } else { - scene.addEnemyModifier(modifier, true, true); + if (modifierType instanceof ModifierTypes.ModifierTypeGenerator) { + const pregenArgs = ("type" in item) && (item.type !== null) ? [item.type] : undefined; + modifierType = modifierType.generateType([], pregenArgs); + } + + const modifier = modifierType && modifierType.withIdFromFunc(modifierFunc).newModifier() as PersistentModifier; + if (modifier) { + modifier.stackCount = item.count || 1; + + if (isPlayer) { + scene.addModifier(modifier, true, false, false, true); + } else { + scene.addEnemyModifier(modifier, true, true); + } } }); } diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 66946d268cb..d5dd9f61340 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -1,14 +1,14 @@ -import BattleScene from "#app/battle-scene.js"; -import { BattlerIndex, BattleType } from "#app/battle.js"; -import { applyPostFaintAbAttrs, PostFaintAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr } from "#app/data/ability.js"; -import { BattlerTagLapseType } from "#app/data/battler-tags.js"; -import { battleSpecDialogue } from "#app/data/dialogue.js"; -import { allMoves, PostVictoryStatChangeAttr } from "#app/data/move.js"; -import { BattleSpec } from "#app/enums/battle-spec.js"; -import { StatusEffect } from "#app/enums/status-effect.js"; -import { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon.js"; -import { getPokemonNameWithAffix } from "#app/messages.js"; -import { PokemonInstantReviveModifier } from "#app/modifier/modifier.js"; +import BattleScene from "#app/battle-scene"; +import { BattlerIndex, BattleType } from "#app/battle"; +import { applyPostFaintAbAttrs, PostFaintAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr } from "#app/data/ability"; +import { BattlerTagLapseType } from "#app/data/battler-tags"; +import { battleSpecDialogue } from "#app/data/dialogue"; +import { allMoves, PostVictoryStatStageChangeAttr } from "#app/data/move"; +import { BattleSpec } from "#app/enums/battle-spec"; +import { StatusEffect } from "#app/enums/status-effect"; +import { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { PokemonInstantReviveModifier } from "#app/modifier/modifier"; import i18next from "i18next"; import { DamagePhase } from "./damage-phase"; import { PokemonPhase } from "./pokemon-phase"; @@ -72,7 +72,7 @@ export class FaintPhase extends PokemonPhase { if (defeatSource?.isOnField()) { applyPostVictoryAbAttrs(PostVictoryAbAttr, defeatSource); const pvmove = allMoves[pokemon.turnData.attacksReceived[0].move]; - const pvattrs = pvmove.getAttrs(PostVictoryStatChangeAttr); + const pvattrs = pvmove.getAttrs(PostVictoryStatStageChangeAttr); if (pvattrs.length) { for (const pvattr of pvattrs) { pvattr.applyPostVictory(defeatSource, defeatSource, pvmove); diff --git a/src/phases/field-phase.ts b/src/phases/field-phase.ts index 02d1f1395d3..b65e903a32b 100644 --- a/src/phases/field-phase.ts +++ b/src/phases/field-phase.ts @@ -1,5 +1,5 @@ +import Pokemon from "#app/field/pokemon.js"; import { BattlePhase } from "./battle-phase"; -import Pokemon from "#app/field/pokemon"; type PokemonFunc = (pokemon: Pokemon) => void; diff --git a/src/phases/stat-change-phase.ts b/src/phases/stat-change-phase.ts deleted file mode 100644 index 3116c49e8ef..00000000000 --- a/src/phases/stat-change-phase.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { BattlerIndex } from "#app/battle"; -import BattleScene from "#app/battle-scene"; -import { applyAbAttrs, applyPostStatChangeAbAttrs, applyPreStatChangeAbAttrs, PostStatChangeAbAttr, ProtectStatAbAttr, StatChangeCopyAbAttr, StatChangeMultiplierAbAttr } from "#app/data/ability"; -import { ArenaTagSide, MistTag } from "#app/data/arena-tag"; -import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "#app/data/battle-stat"; -import Pokemon from "#app/field/pokemon"; -import { getPokemonNameWithAffix } from "#app/messages"; -import { PokemonResetNegativeStatStageModifier } from "#app/modifier/modifier"; -import { handleTutorial, Tutorial } from "#app/tutorial"; -import * as Utils from "#app/utils"; -import i18next from "i18next"; -import { PokemonPhase } from "./pokemon-phase"; - -export type StatChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void; - -export class StatChangePhase extends PokemonPhase { - private stats: BattleStat[]; - private selfTarget: boolean; - private levels: integer; - private showMessage: boolean; - private ignoreAbilities: boolean; - private canBeCopied: boolean; - private onChange: StatChangeCallback | null; - - - constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], levels: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatChangeCallback | null = null) { - super(scene, battlerIndex); - - this.selfTarget = selfTarget; - this.stats = stats; - this.levels = levels; - this.showMessage = showMessage; - this.ignoreAbilities = ignoreAbilities; - this.canBeCopied = canBeCopied; - this.onChange = onChange; - } - - start() { - const pokemon = this.getPokemon(); - - let random = false; - - if (this.stats.length === 1 && this.stats[0] === BattleStat.RAND) { - this.stats[0] = this.getRandomStat(); - random = true; - } - - this.aggregateStatChanges(random); - - if (!pokemon.isActive(true)) { - return this.end(); - } - - const filteredStats = this.stats.map(s => s !== BattleStat.RAND ? s : this.getRandomStat()).filter(stat => { - const cancelled = new Utils.BooleanHolder(false); - - if (!this.selfTarget && this.levels < 0) { - this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled); - } - - if (!cancelled.value && !this.selfTarget && this.levels < 0) { - applyPreStatChangeAbAttrs(ProtectStatAbAttr, this.getPokemon(), stat, cancelled); - } - - return !cancelled.value; - }); - - const levels = new Utils.IntegerHolder(this.levels); - - if (!this.ignoreAbilities) { - applyAbAttrs(StatChangeMultiplierAbAttr, pokemon, null, false, levels); - } - - const battleStats = this.getPokemon().summonData.battleStats; - const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats[stat] + levels.value, 6) : Math.max(battleStats[stat] + levels.value, -6)) - battleStats[stat]); - - this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels); - - const end = () => { - if (this.showMessage) { - const messages = this.getStatChangeMessages(filteredStats, levels.value, relLevels); - for (const message of messages) { - this.scene.queueMessage(message); - } - } - - for (const stat of filteredStats) { - if (levels.value > 0 && pokemon.summonData.battleStats[stat] < 6) { - if (!pokemon.turnData) { - // Temporary fix for missing turn data struct on turn 1 - pokemon.resetTurnData(); - } - pokemon.turnData.battleStatsIncreased = true; - } else if (levels.value < 0 && pokemon.summonData.battleStats[stat] > -6) { - if (!pokemon.turnData) { - // Temporary fix for missing turn data struct on turn 1 - pokemon.resetTurnData(); - } - pokemon.turnData.battleStatsDecreased = true; - } - - pokemon.summonData.battleStats[stat] = Math.max(Math.min(pokemon.summonData.battleStats[stat] + levels.value, 6), -6); - } - - if (levels.value > 0 && this.canBeCopied) { - for (const opponent of pokemon.getOpponents()) { - applyAbAttrs(StatChangeCopyAbAttr, opponent, null, false, this.stats, levels.value); - } - } - - applyPostStatChangeAbAttrs(PostStatChangeAbAttr, pokemon, filteredStats, this.levels, this.selfTarget); - - // Look for any other stat change phases; if this is the last one, do White Herb check - const existingPhase = this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex); - if (!(existingPhase instanceof StatChangePhase)) { - // Apply White Herb if needed - const whiteHerb = this.scene.applyModifier(PokemonResetNegativeStatStageModifier, this.player, pokemon) as PokemonResetNegativeStatStageModifier; - // If the White Herb was applied, consume it - if (whiteHerb) { - --whiteHerb.stackCount; - if (whiteHerb.stackCount <= 0) { - this.scene.removeModifier(whiteHerb); - } - this.scene.updateModifiers(this.player); - } - } - - pokemon.updateInfo(); - - handleTutorial(this.scene, Tutorial.Stat_Change).then(() => super.end()); - }; - - if (relLevels.filter(l => l).length && this.scene.moveAnimations) { - pokemon.enableMask(); - const pokemonMaskSprite = pokemon.maskSprite; - - const tileX = (this.player ? 106 : 236) * pokemon.getSpriteScale() * this.scene.field.scale; - const tileY = ((this.player ? 148 : 84) + (levels.value >= 1 ? 160 : 0)) * pokemon.getSpriteScale() * this.scene.field.scale; - const tileWidth = 156 * this.scene.field.scale * pokemon.getSpriteScale(); - const tileHeight = 316 * this.scene.field.scale * pokemon.getSpriteScale(); - - // On increase, show the red sprite located at ATK - // On decrease, show the blue sprite located at SPD - const spriteColor = levels.value >= 1 ? BattleStat[BattleStat.ATK].toLowerCase() : BattleStat[BattleStat.SPD].toLowerCase(); - const statSprite = this.scene.add.tileSprite(tileX, tileY, tileWidth, tileHeight, "battle_stats", spriteColor); - statSprite.setPipeline(this.scene.fieldSpritePipeline); - statSprite.setAlpha(0); - statSprite.setScale(6); - statSprite.setOrigin(0.5, 1); - - this.scene.playSound(`se/stat_${levels.value >= 1 ? "up" : "down"}`); - - statSprite.setMask(new Phaser.Display.Masks.BitmapMask(this.scene, pokemonMaskSprite ?? undefined)); - - this.scene.tweens.add({ - targets: statSprite, - duration: 250, - alpha: 0.8375, - onComplete: () => { - this.scene.tweens.add({ - targets: statSprite, - delay: 1000, - duration: 250, - alpha: 0 - }); - } - }); - - this.scene.tweens.add({ - targets: statSprite, - duration: 1500, - y: `${levels.value >= 1 ? "-" : "+"}=${160 * 6}` - }); - - this.scene.time.delayedCall(1750, () => { - pokemon.disableMask(); - end(); - }); - } else { - end(); - } - } - - getRandomStat(): BattleStat { - const allStats = Utils.getEnumValues(BattleStat); - return this.getPokemon() ? allStats[this.getPokemon()!.randSeedInt(BattleStat.SPD + 1)] : BattleStat.ATK; // TODO: return default ATK on random? idk... - } - - aggregateStatChanges(random: boolean = false): void { - 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 - && (p.stats[0] === this.stats[0] || (random && p.stats[0] === BattleStat.RAND)) - && p.selfTarget === this.selfTarget && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatChangePhase))) { - if (existingPhase.stats[0] === BattleStat.RAND) { - existingPhase.stats[0] = this.getRandomStat(); - if (existingPhase.stats[0] !== this.stats[0]) { - continue; - } - } - this.levels += existingPhase.levels; - - if (!this.scene.tryRemovePhase(p => p === existingPhase)) { - break; - } - } - } - 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) - && 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)) { - break; - } - } - } - - getStatChangeMessages(stats: BattleStat[], levels: integer, relLevels: integer[]): string[] { - const messages: string[] = []; - - const relLevelStatIndexes = {}; - for (let rl = 0; rl < relLevels.length; rl++) { - const relLevel = relLevels[rl]; - if (!relLevelStatIndexes[relLevel]) { - relLevelStatIndexes[relLevel] = []; - } - relLevelStatIndexes[relLevel].push(rl); - } - - Object.keys(relLevelStatIndexes).forEach(rl => { - const relLevelStats = stats.filter((_, i) => relLevelStatIndexes[rl].includes(i)); - let statsFragment = ""; - - if (relLevelStats.length > 1) { - statsFragment = relLevelStats.length >= 5 - ? i18next.t("battle:stats") - : `${relLevelStats.slice(0, -1).map(s => getBattleStatName(s)).join(", ")}${relLevelStats.length > 2 ? "," : ""} ${i18next.t("battle:statsAnd")} ${getBattleStatName(relLevelStats[relLevelStats.length - 1])}`; - messages.push(getBattleStatLevelChangeDescription(getPokemonNameWithAffix(this.getPokemon()), statsFragment, Math.abs(parseInt(rl)), levels >= 1, relLevelStats.length)); - } else { - statsFragment = getBattleStatName(relLevelStats[0]); - messages.push(getBattleStatLevelChangeDescription(getPokemonNameWithAffix(this.getPokemon()), statsFragment, Math.abs(parseInt(rl)), levels >= 1, relLevelStats.length)); - } - }); - - return messages; - } -} diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts new file mode 100644 index 00000000000..55faaa29903 --- /dev/null +++ b/src/phases/stat-stage-change-phase.ts @@ -0,0 +1,244 @@ +import { BattlerIndex } from "#app/battle"; +import BattleScene from "#app/battle-scene"; +import { applyAbAttrs, applyPostStatStageChangeAbAttrs, applyPreStatStageChangeAbAttrs, PostStatStageChangeAbAttr, ProtectStatAbAttr, StatStageChangeCopyAbAttr, StatStageChangeMultiplierAbAttr } from "#app/data/ability"; +import { ArenaTagSide, MistTag } from "#app/data/arena-tag"; +import Pokemon from "#app/field/pokemon"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { ResetNegativeStatStageModifier } from "#app/modifier/modifier"; +import { handleTutorial, Tutorial } from "#app/tutorial"; +import * as Utils from "#app/utils"; +import i18next from "i18next"; +import { PokemonPhase } from "./pokemon-phase"; +import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat"; + +export type StatStageChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void; + +export class StatStageChangePhase extends PokemonPhase { + private stats: BattleStat[]; + private selfTarget: boolean; + private stages: integer; + private showMessage: boolean; + private ignoreAbilities: boolean; + private canBeCopied: boolean; + private onChange: StatStageChangeCallback | null; + + + constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], stages: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatStageChangeCallback | null = null) { + super(scene, battlerIndex); + + this.selfTarget = selfTarget; + this.stats = stats; + this.stages = stages; + this.showMessage = showMessage; + this.ignoreAbilities = ignoreAbilities; + this.canBeCopied = canBeCopied; + this.onChange = onChange; + } + + start() { + const pokemon = this.getPokemon(); + + if (!pokemon.isActive(true)) { + return this.end(); + } + + let simulate = false; + + const filteredStats = this.stats.filter(stat => { + const cancelled = new Utils.BooleanHolder(false); + + if (!this.selfTarget && this.stages < 0) { + // TODO: Include simulate boolean when tag applications can be simulated + this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled); + } + + if (!cancelled.value && !this.selfTarget && this.stages < 0) { + applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate); + } + + // If one stat stage decrease is cancelled, simulate the rest of the applications + if (cancelled.value) { + simulate = true; + } + + return !cancelled.value; + }); + + const stages = new Utils.IntegerHolder(this.stages); + + if (!this.ignoreAbilities) { + applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages); + } + + const relLevels = filteredStats.map(s => (stages.value >= 1 ? Math.min(pokemon.getStatStage(s) + stages.value, 6) : Math.max(pokemon.getStatStage(s) + stages.value, -6)) - pokemon.getStatStage(s)); + + this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels); + + const end = () => { + if (this.showMessage) { + const messages = this.getStatStageChangeMessages(filteredStats, stages.value, relLevels); + for (const message of messages) { + this.scene.queueMessage(message); + } + } + + for (const s of filteredStats) { + if (stages.value > 0 && pokemon.getStatStage(s) < 6) { + if (!pokemon.turnData) { + // Temporary fix for missing turn data struct on turn 1 + pokemon.resetTurnData(); + } + pokemon.turnData.statStagesIncreased = true; + } else if (stages.value < 0 && pokemon.getStatStage(s) > -6) { + if (!pokemon.turnData) { + // Temporary fix for missing turn data struct on turn 1 + pokemon.resetTurnData(); + } + pokemon.turnData.statStagesDecreased = true; + } + + pokemon.setStatStage(s, pokemon.getStatStage(s) + stages.value); + } + + if (stages.value > 0 && this.canBeCopied) { + for (const opponent of pokemon.getOpponents()) { + applyAbAttrs(StatStageChangeCopyAbAttr, opponent, null, false, this.stats, stages.value); + } + } + + applyPostStatStageChangeAbAttrs(PostStatStageChangeAbAttr, pokemon, filteredStats, this.stages, this.selfTarget); + + // Look for any other stat change phases; if this is the last one, do White Herb check + const existingPhase = this.scene.findPhase(p => p instanceof StatStageChangePhase && p.battlerIndex === this.battlerIndex); + if (!(existingPhase instanceof StatStageChangePhase)) { + // Apply White Herb if needed + const whiteHerb = this.scene.applyModifier(ResetNegativeStatStageModifier, this.player, pokemon) as ResetNegativeStatStageModifier; + // If the White Herb was applied, consume it + if (whiteHerb) { + whiteHerb.stackCount--; + if (whiteHerb.stackCount <= 0) { + this.scene.removeModifier(whiteHerb); + } + this.scene.updateModifiers(this.player); + } + } + + pokemon.updateInfo(); + + handleTutorial(this.scene, Tutorial.Stat_Change).then(() => super.end()); + }; + + if (relLevels.filter(l => l).length && this.scene.moveAnimations) { + pokemon.enableMask(); + const pokemonMaskSprite = pokemon.maskSprite; + + const tileX = (this.player ? 106 : 236) * pokemon.getSpriteScale() * this.scene.field.scale; + const tileY = ((this.player ? 148 : 84) + (stages.value >= 1 ? 160 : 0)) * pokemon.getSpriteScale() * this.scene.field.scale; + const tileWidth = 156 * this.scene.field.scale * pokemon.getSpriteScale(); + const tileHeight = 316 * this.scene.field.scale * pokemon.getSpriteScale(); + + // On increase, show the red sprite located at ATK + // On decrease, show the blue sprite located at SPD + const spriteColor = stages.value >= 1 ? Stat[Stat.ATK].toLowerCase() : Stat[Stat.SPD].toLowerCase(); + const statSprite = this.scene.add.tileSprite(tileX, tileY, tileWidth, tileHeight, "battle_stats", spriteColor); + statSprite.setPipeline(this.scene.fieldSpritePipeline); + statSprite.setAlpha(0); + statSprite.setScale(6); + statSprite.setOrigin(0.5, 1); + + this.scene.playSound(`se/stat_${stages.value >= 1 ? "up" : "down"}`); + + statSprite.setMask(new Phaser.Display.Masks.BitmapMask(this.scene, pokemonMaskSprite ?? undefined)); + + this.scene.tweens.add({ + targets: statSprite, + duration: 250, + alpha: 0.8375, + onComplete: () => { + this.scene.tweens.add({ + targets: statSprite, + delay: 1000, + duration: 250, + alpha: 0 + }); + } + }); + + this.scene.tweens.add({ + targets: statSprite, + duration: 1500, + y: `${stages.value >= 1 ? "-" : "+"}=${160 * 6}` + }); + + this.scene.time.delayedCall(1750, () => { + pokemon.disableMask(); + end(); + }); + } else { + end(); + } + } + + aggregateStatStageChanges(): void { + const accEva: BattleStat[] = [ Stat.ACC, Stat.EVA ]; + const isAccEva = accEva.some(s => this.stats.includes(s)); + let existingPhase: StatStageChangePhase; + if (this.stats.length === 1) { + while ((existingPhase = (this.scene.findPhase(p => p instanceof StatStageChangePhase && p.battlerIndex === this.battlerIndex && p.stats.length === 1 + && (p.stats[0] === this.stats[0]) + && p.selfTarget === this.selfTarget && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatStageChangePhase))) { + this.stages += existingPhase.stages; + + if (!this.scene.tryRemovePhase(p => p === existingPhase)) { + break; + } + } + } + while ((existingPhase = (this.scene.findPhase(p => p instanceof StatStageChangePhase && p.battlerIndex === this.battlerIndex && p.selfTarget === this.selfTarget + && (accEva.some(s => p.stats.includes(s)) === isAccEva) + && p.stages === this.stages && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatStageChangePhase))) { + this.stats.push(...existingPhase.stats); + if (!this.scene.tryRemovePhase(p => p === existingPhase)) { + break; + } + } + } + + getStatStageChangeMessages(stats: BattleStat[], stages: integer, relStages: integer[]): string[] { + const messages: string[] = []; + + const relStageStatIndexes = {}; + for (let rl = 0; rl < relStages.length; rl++) { + const relStage = relStages[rl]; + if (!relStageStatIndexes[relStage]) { + relStageStatIndexes[relStage] = []; + } + relStageStatIndexes[relStage].push(rl); + } + + Object.keys(relStageStatIndexes).forEach(rl => { + const relStageStats = stats.filter((_, i) => relStageStatIndexes[rl].includes(i)); + let statsFragment = ""; + + if (relStageStats.length > 1) { + statsFragment = relStageStats.length >= 5 + ? i18next.t("battle:stats") + : `${relStageStats.slice(0, -1).map(s => i18next.t(getStatKey(s))).join(", ")}${relStageStats.length > 2 ? "," : ""} ${i18next.t("battle:statsAnd")} ${i18next.t(getStatKey(relStageStats[relStageStats.length - 1]))}`; + messages.push(i18next.t(getStatStageChangeDescriptionKey(Math.abs(parseInt(rl)), stages >= 1), { + pokemonNameWithAffix: getPokemonNameWithAffix(this.getPokemon()), + stats: statsFragment, + count: relStageStats.length + })); + } else { + statsFragment = i18next.t(getStatKey(relStageStats[0])); + messages.push(i18next.t(getStatStageChangeDescriptionKey(Math.abs(parseInt(rl)), stages >= 1), { + pokemonNameWithAffix: getPokemonNameWithAffix(this.getPokemon()), + stats: statsFragment, + count: relStageStats.length + })); + } + }); + + return messages; + } +} diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index b2545e9ee30..5c1af4228c6 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -43,8 +43,8 @@ export class TurnStartPhase extends FieldPhase { }, this.scene.currentBattle.turn, this.scene.waveSeed); orderedTargets.sort((a: Pokemon, b: Pokemon) => { - const aSpeed = a?.getBattleStat(Stat.SPD) || 0; - const bSpeed = b?.getBattleStat(Stat.SPD) || 0; + const aSpeed = a?.getEffectiveStat(Stat.SPD) || 0; + const bSpeed = b?.getEffectiveStat(Stat.SPD) || 0; return bSpeed - aSpeed; }); diff --git a/src/system/achv.ts b/src/system/achv.ts index de2862c2813..89e5493eb2e 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -5,9 +5,10 @@ import { pokemonEvolutions } from "#app/data/pokemon-evolutions"; import i18next from "i18next"; import * as Utils from "../utils"; import { PlayerGender } from "#enums/player-gender"; -import { Challenge, FreshStartChallenge, InverseBattleChallenge, SingleGenerationChallenge, SingleTypeChallenge } from "#app/data/challenge"; -import { Challenges } from "#app/enums/challenges"; +import { Challenge, FreshStartChallenge, SingleGenerationChallenge, SingleTypeChallenge, InverseBattleChallenge } from "#app/data/challenge"; import { ConditionFn } from "#app/@types/common"; +import { Stat, getShortenedStatKey } from "#app/enums/stat"; +import { Challenges } from "#app/enums/challenges"; export enum AchvTier { COMMON, @@ -172,13 +173,13 @@ export function getAchievementDescription(localizationKey: string): string { case "10000_DMG": return i18next.t("achv:DamageAchv.description", {context: genderStr, "damageAmount": achvs._10000_DMG.damageAmount.toLocaleString("en-US")}); case "250_HEAL": - return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._250_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")}); + return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._250_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP))}); case "1000_HEAL": - return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._1000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")}); + return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._1000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP))}); case "2500_HEAL": - return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._2500_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")}); + return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._2500_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP))}); case "10000_HEAL": - return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._10000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")}); + return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._10000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP))}); case "LV_100": return i18next.t("achv:LevelAchv.description", {context: genderStr, "level": achvs.LV_100.level}); case "LV_250": @@ -195,7 +196,7 @@ export function getAchievementDescription(localizationKey: string): string { return i18next.t("achv:RibbonAchv.description", {context: genderStr, "ribbonAmount": achvs._75_RIBBONS.ribbonAmount.toLocaleString("en-US")}); case "100_RIBBONS": return i18next.t("achv:RibbonAchv.description", {context: genderStr, "ribbonAmount": achvs._100_RIBBONS.ribbonAmount.toLocaleString("en-US")}); - case "TRANSFER_MAX_BATTLE_STAT": + case "TRANSFER_MAX_STAT_STAGE": return i18next.t("achv:TRANSFER_MAX_BATTLE_STAT.description", { context: genderStr }); case "MAX_FRIENDSHIP": return i18next.t("achv:MAX_FRIENDSHIP.description", { context: genderStr }); @@ -305,7 +306,7 @@ export const achvs = { _50_RIBBONS: new RibbonAchv("50_RIBBONS", "", 50, "ultra_ribbon", 50).setSecret(true), _75_RIBBONS: new RibbonAchv("75_RIBBONS", "", 75, "rogue_ribbon", 75).setSecret(true), _100_RIBBONS: new RibbonAchv("100_RIBBONS", "", 100, "master_ribbon", 100).setSecret(true), - TRANSFER_MAX_BATTLE_STAT: new Achv("TRANSFER_MAX_BATTLE_STAT", "", "TRANSFER_MAX_BATTLE_STAT.description", "baton", 20), + TRANSFER_MAX_STAT_STAGE: new Achv("TRANSFER_MAX_STAT_STAGE", "", "TRANSFER_MAX_STAT_STAGE.description", "baton", 20), MAX_FRIENDSHIP: new Achv("MAX_FRIENDSHIP", "", "MAX_FRIENDSHIP.description", "soothe_bell", 25), MEGA_EVOLVE: new Achv("MEGA_EVOLVE", "", "MEGA_EVOLVE.description", "mega_bracelet", 50), GIGANTAMAX: new Achv("GIGANTAMAX", "", "GIGANTAMAX.description", "dynamax_band", 50), diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 928077608ed..9a743ceb1d2 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -124,7 +124,8 @@ export default class PokemonData { this.summonData = new PokemonSummonData(); if (!forHistory && source.summonData) { - this.summonData.battleStats = source.summonData.battleStats; + this.summonData.stats = source.summonData.stats; + this.summonData.statStages = source.summonData.statStages; this.summonData.moveQueue = source.summonData.moveQueue; this.summonData.disabledMove = source.summonData.disabledMove; this.summonData.disabledTurns = source.summonData.disabledTurns; diff --git a/src/test/abilities/beast_boost.test.ts b/src/test/abilities/beast_boost.test.ts new file mode 100644 index 00000000000..cfe015c822e --- /dev/null +++ b/src/test/abilities/beast_boost.test.ts @@ -0,0 +1,97 @@ +import { Stat } from "#enums/stat"; +import GameManager from "#test/utils/gameManager"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { SPLASH_ONLY } from "../utils/testUtils"; +import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; +import { VictoryPhase } from "#app/phases/victory-phase"; +import { TurnStartPhase } from "#app/phases/turn-start-phase"; +import { BattlerIndex } from "#app/battle"; + +describe("Abilities - Beast Boost", () => { + 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 + .battleType("single") + .enemySpecies(Species.BULBASAUR) + .enemyAbility(Abilities.BEAST_BOOST) + .ability(Abilities.BEAST_BOOST) + .startingLevel(2000) + .moveset([ Moves.FLAMETHROWER ]) + .enemyMoveset(SPLASH_ONLY); + }); + + // Note that both MOXIE and BEAST_BOOST test for their current implementation and not their mainline behavior. + it("should prefer highest stat to boost its corresponding stat stage by 1 when winning a battle", async() => { + // SLOWBRO's highest stat is DEF, so it should be picked here + await game.startBattle([ + Species.SLOWBRO + ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + expect(playerPokemon.getStatStage(Stat.DEF)).toBe(0); + + game.move.select(Moves.FLAMETHROWER); + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase); + + expect(playerPokemon.getStatStage(Stat.DEF)).toBe(1); + }, 20000); + + it("should use in-battle overriden stats when determining the stat stage to raise by 1", async() => { + // If the opponent can GUARD_SPLIT, SLOWBRO's second highest stat should be SPATK + game.override.enemyMoveset(new Array(4).fill(Moves.GUARD_SPLIT)); + + await game.startBattle([ + Species.SLOWBRO + ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(0); + + game.move.select(Moves.FLAMETHROWER); + + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + + await game.phaseInterceptor.runFrom(TurnStartPhase).to(VictoryPhase); + + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); + }, 20000); + + it("should have order preference in case of stat ties", async() => { + // Order preference follows the order of EFFECTIVE_STAT + await game.startBattle([ + Species.SLOWBRO + ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + // Set up tie between SPATK, SPDEF, and SPD, where SPATK should win + vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([ 10000, 1, 1, 100, 100, 100 ]); + + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(0); + + game.move.select(Moves.FLAMETHROWER); + + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase); + + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); + }, 20000); +}); diff --git a/src/test/abilities/contrary.test.ts b/src/test/abilities/contrary.test.ts new file mode 100644 index 00000000000..19ecc7e0240 --- /dev/null +++ b/src/test/abilities/contrary.test.ts @@ -0,0 +1,42 @@ +import { Stat } from "#enums/stat"; +import GameManager from "#test/utils/gameManager"; +import { Abilities } from "#enums/abilities"; +import { Species } from "#enums/species"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { SPLASH_ONLY } from "../utils/testUtils"; + +describe("Abilities - Contrary", () => { + 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 + .battleType("single") + .enemySpecies(Species.BULBASAUR) + .enemyAbility(Abilities.CONTRARY) + .ability(Abilities.INTIMIDATE) + .enemyMoveset(SPLASH_ONLY); + }); + + it("should invert stat changes when applied", async() => { + await game.startBattle([ + Species.SLOWBRO + ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); + }, 20000); +}); diff --git a/src/test/abilities/costar.test.ts b/src/test/abilities/costar.test.ts index 9a4baeef1fb..96ec775f2a0 100644 --- a/src/test/abilities/costar.test.ts +++ b/src/test/abilities/costar.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; @@ -35,7 +35,7 @@ describe("Abilities - COSTAR", () => { test( - "ability copies positive stat changes", + "ability copies positive stat stages", async () => { game.override.enemyAbility(Abilities.BALL_FETCH); @@ -48,8 +48,8 @@ describe("Abilities - COSTAR", () => { game.move.select(Moves.SPLASH, 1); await game.toNextTurn(); - expect(leftPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(+2); - expect(rightPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(0); + expect(leftPokemon.getStatStage(Stat.SPATK)).toBe(2); + expect(rightPokemon.getStatStage(Stat.SPATK)).toBe(0); game.move.select(Moves.SPLASH); await game.phaseInterceptor.to(CommandPhase); @@ -57,14 +57,14 @@ describe("Abilities - COSTAR", () => { await game.phaseInterceptor.to(MessagePhase); [leftPokemon, rightPokemon] = game.scene.getPlayerField(); - expect(leftPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(+2); - expect(rightPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(+2); + expect(leftPokemon.getStatStage(Stat.SPATK)).toBe(2); + expect(rightPokemon.getStatStage(Stat.SPATK)).toBe(2); }, TIMEOUT, ); test( - "ability copies negative stat changes", + "ability copies negative stat stages", async () => { game.override.enemyAbility(Abilities.INTIMIDATE); @@ -72,8 +72,8 @@ describe("Abilities - COSTAR", () => { let [leftPokemon, rightPokemon] = game.scene.getPlayerField(); - expect(leftPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2); - expect(leftPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2); + expect(leftPokemon.getStatStage(Stat.ATK)).toBe(-2); + expect(leftPokemon.getStatStage(Stat.ATK)).toBe(-2); game.move.select(Moves.SPLASH); await game.phaseInterceptor.to(CommandPhase); @@ -81,8 +81,8 @@ describe("Abilities - COSTAR", () => { await game.phaseInterceptor.to(MessagePhase); [leftPokemon, rightPokemon] = game.scene.getPlayerField(); - expect(leftPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2); - expect(rightPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2); + expect(leftPokemon.getStatStage(Stat.ATK)).toBe(-2); + expect(rightPokemon.getStatStage(Stat.ATK)).toBe(-2); }, TIMEOUT, ); diff --git a/src/test/abilities/disguise.test.ts b/src/test/abilities/disguise.test.ts index bbb0a20dc1a..f7c45e91724 100644 --- a/src/test/abilities/disguise.test.ts +++ b/src/test/abilities/disguise.test.ts @@ -1,8 +1,8 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { StatusEffect } from "#app/data/status-effect"; import { toDmgValue } from "#app/utils"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#app/data/status-effect"; +import { Stat } from "#enums/stat"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { SPLASH_ONLY } from "../utils/testUtils"; @@ -36,7 +36,7 @@ describe("Abilities - Disguise", () => { }, TIMEOUT); it("takes no damage from attacking move and transforms to Busted form, takes 1/8 max HP damage from the disguise breaking", async () => { - await game.startBattle(); + await game.classicMode.startBattle(); const mimikyu = game.scene.getEnemyPokemon()!; const maxHp = mimikyu.getMaxHp(); @@ -53,7 +53,7 @@ describe("Abilities - Disguise", () => { }, TIMEOUT); it("doesn't break disguise when attacked with ineffective move", async () => { - await game.startBattle(); + await game.classicMode.startBattle(); const mimikyu = game.scene.getEnemyPokemon()!; @@ -67,9 +67,9 @@ describe("Abilities - Disguise", () => { }, TIMEOUT); it("takes no damage from the first hit of a multihit move and transforms to Busted form, then takes damage from the second hit", async () => { - game.override.moveset([Moves.SURGING_STRIKES]); + game.override.moveset([ Moves.SURGING_STRIKES ]); game.override.enemyLevel(5); - await game.startBattle(); + await game.classicMode.startBattle(); const mimikyu = game.scene.getEnemyPokemon()!; const maxHp = mimikyu.getMaxHp(); @@ -91,7 +91,7 @@ describe("Abilities - Disguise", () => { }, TIMEOUT); it("takes effects from status moves and damage from status effects", async () => { - await game.startBattle(); + await game.classicMode.startBattle(); const mimikyu = game.scene.getEnemyPokemon()!; expect(mimikyu.hp).toBe(mimikyu.getMaxHp()); @@ -102,7 +102,7 @@ describe("Abilities - Disguise", () => { expect(mimikyu.formIndex).toBe(disguisedForm); expect(mimikyu.status?.effect).toBe(StatusEffect.POISON); - expect(mimikyu.summonData.battleStats[BattleStat.SPD]).toBe(-1); + expect(mimikyu.getStatStage(Stat.SPD)).toBe(-1); expect(mimikyu.hp).toBeLessThan(mimikyu.getMaxHp()); }, TIMEOUT); @@ -110,7 +110,7 @@ describe("Abilities - Disguise", () => { game.override.enemyMoveset(Array(4).fill(Moves.SHADOW_SNEAK)); game.override.starterSpecies(0); - await game.startBattle([Species.MIMIKYU, Species.FURRET]); + await game.classicMode.startBattle([ Species.MIMIKYU, Species.FURRET ]); const mimikyu = game.scene.getPlayerPokemon()!; const maxHp = mimikyu.getMaxHp(); @@ -136,7 +136,7 @@ describe("Abilities - Disguise", () => { game.override.starterForms({ [Species.MIMIKYU]: bustedForm }); - await game.startBattle([Species.FURRET, Species.MIMIKYU]); + await game.classicMode.startBattle([ Species.FURRET, Species.MIMIKYU ]); const mimikyu = game.scene.getParty()[1]!; expect(mimikyu.formIndex).toBe(bustedForm); @@ -155,7 +155,7 @@ describe("Abilities - Disguise", () => { [Species.MIMIKYU]: bustedForm }); - await game.startBattle(); + await game.classicMode.startBattle(); const mimikyu = game.scene.getPlayerPokemon()!; @@ -175,7 +175,7 @@ describe("Abilities - Disguise", () => { [Species.MIMIKYU]: bustedForm }); - await game.startBattle([Species.MIMIKYU, Species.FURRET]); + await game.classicMode.startBattle([ Species.MIMIKYU, Species.FURRET ]); const mimikyu1 = game.scene.getPlayerPokemon()!; @@ -194,7 +194,7 @@ describe("Abilities - Disguise", () => { it("doesn't faint twice when fainting due to Disguise break damage, nor prevent faint from Disguise break damage if using Endure", async () => { game.override.enemyMoveset(Array(4).fill(Moves.ENDURE)); - await game.startBattle(); + await game.classicMode.startBattle(); const mimikyu = game.scene.getEnemyPokemon()!; mimikyu.hp = 1; diff --git a/src/test/abilities/flower_gift.test.ts b/src/test/abilities/flower_gift.test.ts index f8c1141386d..de07bd29478 100644 --- a/src/test/abilities/flower_gift.test.ts +++ b/src/test/abilities/flower_gift.test.ts @@ -49,16 +49,16 @@ describe("Abilities - Flower Gift", () => { }); // TODO: Uncomment expect statements when the ability is implemented - currently does not increase stats of allies - it("increases the Attack and Special Defense stats of the Pokémon with this Ability and its allies by 1.5× during Harsh Sunlight", async () => { + it("increases the ATK and SPDEF stat stages of the Pokémon with this Ability and its allies by 1.5× during Harsh Sunlight", async () => { game.override.battleType("double"); await game.classicMode.startBattle([Species.CHERRIM, Species.MAGIKARP]); const [ cherrim ] = game.scene.getPlayerField(); - const cherrimAtkStat = cherrim.getBattleStat(Stat.ATK); - const cherrimSpDefStat = cherrim.getBattleStat(Stat.SPDEF); + const cherrimAtkStat = cherrim.getEffectiveStat(Stat.ATK); + const cherrimSpDefStat = cherrim.getEffectiveStat(Stat.SPDEF); - // const magikarpAtkStat = magikarp.getBattleStat(Stat.ATK);; - // const magikarpSpDefStat = magikarp.getBattleStat(Stat.SPDEF); + // const magikarpAtkStat = magikarp.getEffectiveStat(Stat.ATK);; + // const magikarpSpDefStat = magikarp.getEffectiveStat(Stat.SPDEF); game.move.select(Moves.SUNNY_DAY, 0); game.move.select(Moves.SPLASH, 1); @@ -67,10 +67,10 @@ describe("Abilities - Flower Gift", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(cherrim.formIndex).toBe(SUNSHINE_FORM); - expect(cherrim.getBattleStat(Stat.ATK)).toBe(Math.floor(cherrimAtkStat * 1.5)); - expect(cherrim.getBattleStat(Stat.SPDEF)).toBe(Math.floor(cherrimSpDefStat * 1.5)); - // expect(magikarp.getBattleStat(Stat.ATK)).toBe(Math.floor(magikarpAtkStat * 1.5)); - // expect(magikarp.getBattleStat(Stat.SPDEF)).toBe(Math.floor(magikarpSpDefStat * 1.5)); + expect(cherrim.getEffectiveStat(Stat.ATK)).toBe(Math.floor(cherrimAtkStat * 1.5)); + expect(cherrim.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(cherrimSpDefStat * 1.5)); + // expect(magikarp.getEffectiveStat(Stat.ATK)).toBe(Math.floor(magikarpAtkStat * 1.5)); + // expect(magikarp.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(magikarpSpDefStat * 1.5)); }); it("changes the Pokemon's form during Harsh Sunlight", async () => { diff --git a/src/test/abilities/gulp_missile.test.ts b/src/test/abilities/gulp_missile.test.ts index a451d290906..286c3af1c56 100644 --- a/src/test/abilities/gulp_missile.test.ts +++ b/src/test/abilities/gulp_missile.test.ts @@ -1,4 +1,3 @@ -import { BattleStat } from "#app/data/battle-stat"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { StatusEffect } from "#app/enums/status-effect"; import Pokemon from "#app/field/pokemon"; @@ -13,6 +12,7 @@ import { Species } from "#enums/species"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { SPLASH_ONLY } from "../utils/testUtils"; +import { Stat } from "#enums/stat"; describe("Abilities - Gulp Missile", () => { let phaserGame: Phaser.Game; @@ -107,7 +107,7 @@ describe("Abilities - Gulp Missile", () => { expect(cramorant.formIndex).toBe(GULPING_FORM); }); - it("deals ¼ of the attacker's maximum HP when hit by a damaging attack", async () => { + it("deals 1/4 of the attacker's maximum HP when hit by a damaging attack", async () => { game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); await game.startBattle([Species.CRAMORANT]); @@ -139,7 +139,7 @@ describe("Abilities - Gulp Missile", () => { expect(cramorant.formIndex).toBe(GULPING_FORM); }); - it("lowers the attacker's Defense by 1 stage when hit in Gulping form", async () => { + it("lowers attacker's DEF stat stage by 1 when hit in Gulping form", async () => { game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); await game.startBattle([Species.CRAMORANT]); @@ -158,7 +158,7 @@ describe("Abilities - Gulp Missile", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy)); - expect(enemy.summonData.battleStats[BattleStat.DEF]).toBe(-1); + expect(enemy.getStatStage(Stat.DEF)).toBe(-1); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeUndefined(); expect(cramorant.formIndex).toBe(NORMAL_FORM); }); @@ -219,7 +219,7 @@ describe("Abilities - Gulp Missile", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(enemy.hp).toBe(enemyHpPreEffect); - expect(enemy.summonData.battleStats[BattleStat.DEF]).toBe(-1); + expect(enemy.getStatStage(Stat.DEF)).toBe(-1); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeUndefined(); expect(cramorant.formIndex).toBe(NORMAL_FORM); }); diff --git a/src/test/abilities/hustle.test.ts b/src/test/abilities/hustle.test.ts index 276edb691c9..ff96b98c7ac 100644 --- a/src/test/abilities/hustle.test.ts +++ b/src/test/abilities/hustle.test.ts @@ -26,7 +26,7 @@ describe("Abilities - Hustle", () => { game = new GameManager(phaserGame); game.override .ability(Abilities.HUSTLE) - .moveset([Moves.TACKLE, Moves.GIGA_DRAIN, Moves.FISSURE]) + .moveset([ Moves.TACKLE, Moves.GIGA_DRAIN, Moves.FISSURE ]) .disableCrits() .battleType("single") .enemyMoveset(SPLASH_ONLY) @@ -39,13 +39,13 @@ describe("Abilities - Hustle", () => { const pikachu = game.scene.getPlayerPokemon()!; const atk = pikachu.stats[Stat.ATK]; - vi.spyOn(pikachu, "getBattleStat"); + vi.spyOn(pikachu, "getEffectiveStat"); game.move.select(Moves.TACKLE); await game.move.forceHit(); await game.phaseInterceptor.to("DamagePhase"); - expect(pikachu.getBattleStat).toHaveReturnedWith(Math.floor(atk * 1.5)); + expect(pikachu.getEffectiveStat).toHaveReturnedWith(Math.floor(atk * 1.5)); }); it("lowers the accuracy of the user's physical moves by 20%", async () => { @@ -65,13 +65,13 @@ describe("Abilities - Hustle", () => { const pikachu = game.scene.getPlayerPokemon()!; const spatk = pikachu.stats[Stat.SPATK]; - vi.spyOn(pikachu, "getBattleStat"); + vi.spyOn(pikachu, "getEffectiveStat"); vi.spyOn(pikachu, "getAccuracyMultiplier"); game.move.select(Moves.GIGA_DRAIN); await game.phaseInterceptor.to("DamagePhase"); - expect(pikachu.getBattleStat).toHaveReturnedWith(spatk); + expect(pikachu.getEffectiveStat).toHaveReturnedWith(spatk); expect(pikachu.getAccuracyMultiplier).toHaveReturnedWith(1); }); diff --git a/src/test/abilities/hyper_cutter.test.ts b/src/test/abilities/hyper_cutter.test.ts index 28fcc2f6085..64e04ac2fd3 100644 --- a/src/test/abilities/hyper_cutter.test.ts +++ b/src/test/abilities/hyper_cutter.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -51,7 +51,7 @@ describe("Abilities - Hyper Cutter", () => { game.move.select(Moves.STRING_SHOT); await game.toNextTurn(); - expect(enemy.summonData.battleStats[BattleStat.ATK]).toEqual(0); - [BattleStat.ACC, BattleStat.DEF, BattleStat.EVA, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD].forEach((stat: number) => expect(enemy.summonData.battleStats[stat]).toBeLessThan(0)); + expect(enemy.getStatStage(Stat.ATK)).toEqual(0); + [Stat.ACC, Stat.DEF, Stat.EVA, Stat.SPATK, Stat.SPDEF, Stat.SPD].forEach((stat: number) => expect(enemy.getStatStage(stat)).toBeLessThan(0)); }); }); diff --git a/src/test/abilities/imposter.test.ts b/src/test/abilities/imposter.test.ts new file mode 100644 index 00000000000..2857f80632a --- /dev/null +++ b/src/test/abilities/imposter.test.ts @@ -0,0 +1,101 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import { Species } from "#enums/species"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { Moves } from "#enums/moves"; +import { Stat, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; +import { Abilities } from "#enums/abilities"; +import { SPLASH_ONLY } from "../utils/testUtils"; + +// TODO: Add more tests once Imposter is fully implemented +describe("Abilities - Imposter", () => { + 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 + .battleType("single") + .enemySpecies(Species.MEW) + .enemyLevel(200) + .enemyAbility(Abilities.BEAST_BOOST) + .enemyPassiveAbility(Abilities.BALL_FETCH) + .enemyMoveset(SPLASH_ONLY) + .ability(Abilities.IMPOSTER) + .moveset(SPLASH_ONLY); + }); + + it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => { + await game.startBattle([ + Species.DITTO + ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to(TurnEndPhase); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + expect(player.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId); + expect(player.getAbility()).toBe(enemy.getAbility()); + expect(player.getGender()).toBe(enemy.getGender()); + + expect(player.getStat(Stat.HP, false)).not.toBe(enemy.getStat(Stat.HP)); + for (const s of EFFECTIVE_STATS) { + expect(player.getStat(s, false)).toBe(enemy.getStat(s, false)); + } + + for (const s of BATTLE_STATS) { + expect(player.getStatStage(s)).toBe(enemy.getStatStage(s)); + } + + const playerMoveset = player.getMoveset(); + const enemyMoveset = player.getMoveset(); + + for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) { + // TODO: Checks for 5 PP should be done here when that gets addressed + expect(playerMoveset[i]?.moveId).toBe(enemyMoveset[i]?.moveId); + } + + const playerTypes = player.getTypes(); + const enemyTypes = enemy.getTypes(); + + for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) { + expect(playerTypes[i]).toBe(enemyTypes[i]); + } + }, 20000); + + it("should copy in-battle overridden stats", async () => { + game.override.enemyMoveset(new Array(4).fill(Moves.POWER_SPLIT)); + + await game.startBattle([ + Species.DITTO + ]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + const avgAtk = Math.floor((player.getStat(Stat.ATK, false) + enemy.getStat(Stat.ATK, false)) / 2); + const avgSpAtk = Math.floor((player.getStat(Stat.SPATK, false) + enemy.getStat(Stat.SPATK, false)) / 2); + + game.move.select(Moves.TACKLE); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStat(Stat.ATK, false)).toBe(avgAtk); + expect(enemy.getStat(Stat.ATK, false)).toBe(avgAtk); + + expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); + expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk); + }); +}); diff --git a/src/test/abilities/intimidate.test.ts b/src/test/abilities/intimidate.test.ts index bc5b5ab1a7d..f90ba6c0e1e 100644 --- a/src/test/abilities/intimidate.test.ts +++ b/src/test/abilities/intimidate.test.ts @@ -1,17 +1,13 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { Status, StatusEffect } from "#app/data/status-effect"; -import { GameModes, getGameMode } from "#app/game-mode"; -import { EncounterPhase } from "#app/phases/encounter-phase"; -import { SelectStarterPhase } from "#app/phases/select-starter-phase"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#test/utils/gameManager"; import { Mode } from "#app/ui/ui"; +import { Stat } from "#enums/stat"; +import { getMovePosition } from "#test/utils/gameManagerUtils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import GameManager from "#test/utils/gameManager"; -import { generateStarter } from "#test/utils/gameManagerUtils"; import { SPLASH_ONLY } from "#test/utils/testUtils"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Abilities - Intimidate", () => { let phaserGame: Phaser.Game; @@ -29,257 +25,113 @@ describe("Abilities - Intimidate", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override - .battleType("single") - .enemySpecies(Species.MAGIKARP) + game.override.battleType("single") + .enemySpecies(Species.RATTATA) .enemyAbility(Abilities.INTIMIDATE) + .enemyPassiveAbility(Abilities.HYDRATION) .ability(Abilities.INTIMIDATE) - .moveset([Moves.SPLASH, Moves.AERIAL_ACE]) + .startingWave(3) .enemyMoveset(SPLASH_ONLY); }); - it("single - wild with switch", async () => { - await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); - - let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); - - let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - - game.doSwitchPokemon(1); - await game.phaseInterceptor.to("CommandPhase"); - - battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(0); - - battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2); - }, 20000); - - it("single - boss should only trigger once then switch", async () => { - game.override.startingWave(10); - await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); - - let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); - - let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - - game.doSwitchPokemon(1); - await game.phaseInterceptor.to("CommandPhase"); - - battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(0); - - battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2); - }, 20000); - - it("single - trainer should only trigger once with switch", async () => { - game.override.startingWave(5); - await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); - - let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); - - let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - - game.doSwitchPokemon(1); - await game.phaseInterceptor.to("CommandPhase"); - - battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(0); - - battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2); - }, 200000); - - it("double - trainer should only trigger once per pokemon", async () => { - game.override - .battleType("double") - .startingWave(5); - await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); - - const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2); - - const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; - expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2); - - const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2); - - const battleStatsPokemon2 = game.scene.getParty()[1].summonData.battleStats; - expect(battleStatsPokemon2[BattleStat.ATK]).toBe(-2); - }, 20000); - - it("double - wild: should only trigger once per pokemon", async () => { - game.override.battleType("double"); - await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); - - const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2); - - const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; - expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2); - - const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2); - - const battleStatsPokemon2 = game.scene.getParty()[1].summonData.battleStats; - expect(battleStatsPokemon2[BattleStat.ATK]).toBe(-2); - }, 20000); - - it("double - boss: should only trigger once per pokemon", async () => { - game.override - .battleType("double") - .startingWave(10); - await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); - - const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2); - - const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; - expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2); - - const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2); - - const battleStatsPokemon2 = game.scene.getParty()[1].summonData.battleStats; - expect(battleStatsPokemon2[BattleStat.ATK]).toBe(-2); - }, 20000); - - it("single - wild next wave opp triger once, us: none", async () => { - await game.startBattle([Species.MIGHTYENA]); - - let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); - - let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - - game.move.select(Moves.AERIAL_ACE); - await game.phaseInterceptor.to("DamagePhase"); - await game.doKillOpponents(); - await game.toNextWave(); - - battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2); - - battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(0); - }, 20000); - - it("single - wild next turn - no retrigger on next turn", async () => { - await game.startBattle([Species.MIGHTYENA]); - - let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); - - let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - - game.move.select(Moves.SPLASH); - await game.toNextTurn(); - - battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - - battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); - }, 20000); - - it("single - trainer should only trigger once and each time he switch", async () => { - game.override - .enemyMoveset(Array(4).fill(Moves.VOLT_SWITCH)) - .startingWave(5); - await game.startBattle([Species.MIGHTYENA]); - - let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); - - let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - - game.move.select(Moves.SPLASH); - await game.toNextTurn(); - - battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2); - - battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(0); - - game.move.select(Moves.SPLASH); - await game.toNextTurn(); - - battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-3); - - battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(0); - }, 200000); - - it("single - trainer should only trigger once whatever turn we are", async () => { - game.override.startingWave(5); - await game.startBattle([Species.MIGHTYENA]); - - const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - - game.move.select(Moves.SPLASH); - await game.toNextTurn(); - - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); - }, 20000); - - it("double - wild vs only 1 on player side", async () => { - game.override.battleType("double"); - await game.classicMode.runToSummon([Species.MIGHTYENA]); + it("should lower ATK stat stage by 1 of enemy Pokemon on entry and player switch", async () => { + await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); + game.onNextPrompt( + "CheckSwitchPhase", + Mode.CONFIRM, + () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, + () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("TurnInitPhase") + ); await game.phaseInterceptor.to("CommandPhase", false); - const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); + let playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; - const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; - expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-1); + expect(playerPokemon.species.speciesId).toBe(Species.MIGHTYENA); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); - const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2); + game.doSwitchPokemon(1); + await game.phaseInterceptor.run("CommandPhase"); + await game.phaseInterceptor.to("CommandPhase"); + + playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.species.speciesId).toBe(Species.POOCHYENA); + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2); }, 20000); - it("double - wild vs only 1 alive on player side", async () => { - game.override.battleType("double"); - await game.runToTitle(); - - game.onNextPrompt("TitlePhase", Mode.TITLE, () => { - game.scene.gameMode = getGameMode(GameModes.CLASSIC); - const starters = generateStarter(game.scene, [Species.MIGHTYENA, Species.POOCHYENA]); - const selectStarterPhase = new SelectStarterPhase(game.scene); - game.scene.pushPhase(new EncounterPhase(game.scene, false)); - selectStarterPhase.initBattle(starters); - game.scene.getParty()[1].hp = 0; - game.scene.getParty()[1].status = new Status(StatusEffect.FAINT); - }); - - await game.phaseInterceptor.run(EncounterPhase); - + it("should lower ATK stat stage by 1 for every enemy Pokemon in a double battle on entry", async () => { + game.override.battleType("double") + .startingWave(3); + await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); + game.onNextPrompt( + "CheckSwitchPhase", + Mode.CONFIRM, + () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, + () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("TurnInitPhase") + ); await game.phaseInterceptor.to("CommandPhase", false); - const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); + const playerField = game.scene.getPlayerField()!; + const enemyField = game.scene.getEnemyField()!; - const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; - expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-1); - - const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2); + expect(enemyField[0].getStatStage(Stat.ATK)).toBe(-2); + expect(enemyField[1].getStatStage(Stat.ATK)).toBe(-2); + expect(playerField[0].getStatStage(Stat.ATK)).toBe(-2); + expect(playerField[1].getStatStage(Stat.ATK)).toBe(-2); }, 20000); + + it("should not activate again if there is no switch or new entry", async () => { + game.override.startingWave(2); + game.override.moveset([Moves.SPLASH]); + await game.classicMode.startBattle([ Species.MIGHTYENA, Species.POOCHYENA ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); + }, 20000); + + it("should lower ATK stat stage by 1 for every switch", async () => { + game.override.moveset([Moves.SPLASH]) + .enemyMoveset(new Array(4).fill(Moves.VOLT_SWITCH)) + .startingWave(5); + await game.classicMode.startBattle([ Species.MIGHTYENA, Species.POOCHYENA ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + let enemyPokemon = game.scene.getEnemyPokemon()!; + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); + + game.move.select(getMovePosition(game.scene, 0, Moves.SPLASH)); + await game.toNextTurn(); + + enemyPokemon = game.scene.getEnemyPokemon()!; + + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-2); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + enemyPokemon = game.scene.getEnemyPokemon()!; + + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-3); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + }, 200000); }); diff --git a/src/test/abilities/intrepid_sword.test.ts b/src/test/abilities/intrepid_sword.test.ts index 18d6c04adbc..7bf0654276c 100644 --- a/src/test/abilities/intrepid_sword.test.ts +++ b/src/test/abilities/intrepid_sword.test.ts @@ -1,8 +1,8 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; +import GameManager from "#test/utils/gameManager"; import { CommandPhase } from "#app/phases/command-phase"; import { Abilities } from "#enums/abilities"; import { Species } from "#enums/species"; -import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -29,14 +29,17 @@ describe("Abilities - Intrepid Sword", () => { game.override.ability(Abilities.INTREPID_SWORD); }); - it("INTREPID SWORD on player", async() => { + it("should raise ATK stat stage by 1 on entry", async() => { await game.classicMode.runToSummon([ Species.ZACIAN, ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + await game.phaseInterceptor.to(CommandPhase, false); - const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(1); - const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(1); + + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); }, 20000); }); diff --git a/src/test/abilities/moody.test.ts b/src/test/abilities/moody.test.ts index 9e936e8100a..5c46ea68ec5 100644 --- a/src/test/abilities/moody.test.ts +++ b/src/test/abilities/moody.test.ts @@ -1,18 +1,16 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import { SPLASH_ONLY } from "#test/utils/testUtils"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Abilities - Moody", () => { let phaserGame: Phaser.Game; let game: GameManager; - const battleStatsArray = [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD]; - beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -30,63 +28,61 @@ describe("Abilities - Moody", () => { .battleType("single") .enemySpecies(Species.RATTATA) .enemyAbility(Abilities.BALL_FETCH) - .enemyPassiveAbility(Abilities.HYDRATION) .ability(Abilities.MOODY) .enemyMoveset(SPLASH_ONLY) .moveset(SPLASH_ONLY); }); - it( - "should increase one BattleStat by 2 stages and decrease a different BattleStat by 1 stage", + it("should increase one stat stage by 2 and decrease a different stat stage by 1", async () => { - await game.startBattle(); + await game.classicMode.startBattle(); const playerPokemon = game.scene.getPlayerPokemon()!; game.move.select(Moves.SPLASH); await game.toNextTurn(); // Find the increased and decreased stats, make sure they are different. - const statChanges = playerPokemon.summonData.battleStats; - const changedStats = battleStatsArray.filter(bs => statChanges[bs] === 2 || statChanges[bs] === -1); + const changedStats = EFFECTIVE_STATS.filter(s => playerPokemon.getStatStage(s) === 2 || playerPokemon.getStatStage(s) === -1); expect(changedStats).toBeTruthy(); expect(changedStats.length).toBe(2); expect(changedStats[0] !== changedStats[1]).toBeTruthy(); }); - it( - "should only increase one BattleStat by 2 stages if all BattleStats are at -6", + it("should only increase one stat stage by 2 if all stat stages are at -6", async () => { - await game.startBattle(); + await game.classicMode.startBattle(); const playerPokemon = game.scene.getPlayerPokemon()!; - // Set all BattleStats to -6 - battleStatsArray.forEach(bs => playerPokemon.summonData.battleStats[bs] = -6); + + // Set all stat stages to -6 + vi.spyOn(playerPokemon.summonData, "statStages", "get").mockReturnValue(new Array(BATTLE_STATS.length).fill(-6)); game.move.select(Moves.SPLASH); await game.toNextTurn(); - // Should increase one BattleStat by 2 (from -6, meaning it will be -4) - const increasedStat = battleStatsArray.filter(bs => playerPokemon.summonData.battleStats[bs] === -4); + // Should increase one stat stage by 2 (from -6, meaning it will be -4) + const increasedStat = EFFECTIVE_STATS.filter(s => playerPokemon.getStatStage(s) === -4); expect(increasedStat).toBeTruthy(); expect(increasedStat.length).toBe(1); }); - it( - "should only decrease one BattleStat by 1 stage if all BattleStats are at 6", + it("should only decrease one stat stage by 1 stage if all stat stages are at 6", async () => { - await game.startBattle(); + await game.classicMode.startBattle(); const playerPokemon = game.scene.getPlayerPokemon()!; - // Set all BattleStats to 6 - battleStatsArray.forEach(bs => playerPokemon.summonData.battleStats[bs] = 6); + + // Set all stat stages to 6 + vi.spyOn(playerPokemon.summonData, "statStages", "get").mockReturnValue(new Array(BATTLE_STATS.length).fill(6)); game.move.select(Moves.SPLASH); await game.toNextTurn(); - // Should decrease one BattleStat by 1 (from 6, meaning it will be 5) - const decreasedStat = battleStatsArray.filter(bs => playerPokemon.summonData.battleStats[bs] === 5); + // Should decrease one stat stage by 1 (from 6, meaning it will be 5) + const decreasedStat = EFFECTIVE_STATS.filter(s => playerPokemon.getStatStage(s) === 5); + expect(decreasedStat).toBeTruthy(); expect(decreasedStat.length).toBe(1); }); diff --git a/src/test/abilities/moxie.test.ts b/src/test/abilities/moxie.test.ts index 6a1838c9a98..e713d78f39e 100644 --- a/src/test/abilities/moxie.test.ts +++ b/src/test/abilities/moxie.test.ts @@ -1,14 +1,15 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { Stat } from "#app/data/pokemon-stat"; -import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; -import { VictoryPhase } from "#app/phases/victory-phase"; +import { Stat } from "#enums/stat"; +import GameManager from "#test/utils/gameManager"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - +import { SPLASH_ONLY } from "../utils/testUtils"; +import { BattlerIndex } from "#app/battle"; +import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; +import { VictoryPhase } from "#app/phases/victory-phase"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; describe("Abilities - Moxie", () => { let phaserGame: Phaser.Game; @@ -32,23 +33,47 @@ describe("Abilities - Moxie", () => { game.override.enemyAbility(Abilities.MOXIE); game.override.ability(Abilities.MOXIE); game.override.startingLevel(2000); - game.override.moveset([moveToUse]); - game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + game.override.moveset([ moveToUse ]); + game.override.enemyMoveset(SPLASH_ONLY); }); - it("MOXIE", async () => { + it("should raise ATK stat stage by 1 when winning a battle", async() => { const moveToUse = Moves.AERIAL_ACE; await game.startBattle([ Species.MIGHTYENA, Species.MIGHTYENA, ]); - let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[Stat.ATK]).toBe(0); + const playerPokemon = game.scene.getPlayerPokemon()!; + + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0); game.move.select(moveToUse); await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase); - battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(1); + + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1); + }, 20000); + + // TODO: Activate this test when MOXIE is corrected to work on faint and not on battle victory + it.todo("should raise ATK stat stage by 1 when defeating an ally Pokemon", async() => { + game.override.battleType("double"); + const moveToUse = Moves.AERIAL_ACE; + await game.startBattle([ + Species.MIGHTYENA, + Species.MIGHTYENA, + ]); + + const [ firstPokemon, secondPokemon ] = game.scene.getPlayerField(); + + expect(firstPokemon.getStatStage(Stat.ATK)).toBe(0); + + secondPokemon.hp = 1; + + game.move.select(moveToUse); + game.selectTarget(BattlerIndex.PLAYER_2); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(firstPokemon.getStatStage(Stat.ATK)).toBe(1); }, 20000); }); diff --git a/src/test/abilities/mycelium_might.test.ts b/src/test/abilities/mycelium_might.test.ts index d5bea185f59..d8947935880 100644 --- a/src/test/abilities/mycelium_might.test.ts +++ b/src/test/abilities/mycelium_might.test.ts @@ -1,10 +1,10 @@ -import { BattleStat } from "#app/data/battle-stat"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { TurnStartPhase } from "#app/phases/turn-start-phase"; +import GameManager from "#test/utils/gameManager"; import { Abilities } from "#enums/abilities"; +import { Stat } from "#enums/stat"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -58,8 +58,9 @@ describe("Abilities - Mycelium Might", () => { expect(speedOrder).toEqual([playerIndex, enemyIndex]); expect(commandOrder).toEqual([enemyIndex, playerIndex]); await game.phaseInterceptor.to(TurnEndPhase); - // Despite the opponent's ability (Clear Body), its attack stat is still reduced. - expect(enemyPokemon?.summonData.battleStats[BattleStat.ATK]).toBe(-1); + + // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. + expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1); }, 20000); it("will still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => { @@ -81,8 +82,8 @@ describe("Abilities - Mycelium Might", () => { expect(speedOrder).toEqual([playerIndex, enemyIndex]); expect(commandOrder).toEqual([playerIndex, enemyIndex]); await game.phaseInterceptor.to(TurnEndPhase); - // Despite the opponent's ability (Clear Body), its attack stat is still reduced. - expect(enemyPokemon?.summonData.battleStats[BattleStat.ATK]).toBe(-1); + // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. + expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1); }, 20000); it("will not affect non-status moves", async () => { diff --git a/src/test/abilities/parental_bond.test.ts b/src/test/abilities/parental_bond.test.ts index 1404f597ccf..e3c6c8ec5bb 100644 --- a/src/test/abilities/parental_bond.test.ts +++ b/src/test/abilities/parental_bond.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { StatusEffect } from "#app/data/status-effect"; import { Type } from "#app/data/type"; import { BattlerTagType } from "#app/enums/battler-tag-type"; @@ -96,7 +96,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to(BerryPhase, false); expect(leadPokemon.turnData.hitCount).toBe(2); - expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(2); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2); }, TIMEOUT ); @@ -116,7 +116,7 @@ describe("Abilities - Parental Bond", () => { game.move.select(Moves.BABY_DOLL_EYES); await game.phaseInterceptor.to(BerryPhase, false); - expect(enemyPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-1); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); }, TIMEOUT ); @@ -568,7 +568,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to(BerryPhase, false); - expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-1); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(-1); }, TIMEOUT ); @@ -590,7 +590,7 @@ describe("Abilities - Parental Bond", () => { await game.phaseInterceptor.to(BerryPhase, false); - expect(enemyPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(1); + expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(1); }, TIMEOUT ); diff --git a/src/test/abilities/sand_veil.test.ts b/src/test/abilities/sand_veil.test.ts index 2336e2b50de..da9fdcc01ab 100644 --- a/src/test/abilities/sand_veil.test.ts +++ b/src/test/abilities/sand_veil.test.ts @@ -1,5 +1,5 @@ -import { BattleStatMultiplierAbAttr, allAbilities } from "#app/data/ability"; -import { BattleStat } from "#app/data/battle-stat"; +import { StatMultiplierAbAttr, allAbilities } from "#app/data/ability"; +import { Stat } from "#enums/stat"; import { WeatherType } from "#app/data/weather"; import { CommandPhase } from "#app/phases/command-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; @@ -49,10 +49,10 @@ describe("Abilities - Sand Veil", () => { vi.spyOn(leadPokemon[0], "getAbility").mockReturnValue(allAbilities[Abilities.SAND_VEIL]); - const sandVeilAttr = allAbilities[Abilities.SAND_VEIL].getAttrs(BattleStatMultiplierAbAttr)[0]; - vi.spyOn(sandVeilAttr, "applyBattleStat").mockImplementation( - (pokemon, passive, simulated, battleStat, statValue, args) => { - if (battleStat === BattleStat.EVA && game.scene.arena.weather?.weatherType === WeatherType.SANDSTORM) { + const sandVeilAttr = allAbilities[Abilities.SAND_VEIL].getAttrs(StatMultiplierAbAttr)[0]; + vi.spyOn(sandVeilAttr, "applyStatStage").mockImplementation( + (_pokemon, _passive, _simulated, stat, statValue, _args) => { + if (stat === Stat.EVA && game.scene.arena.weather?.weatherType === WeatherType.SANDSTORM) { statValue.value *= -1; // will make all attacks miss return true; } diff --git a/src/test/abilities/sap_sipper.test.ts b/src/test/abilities/sap_sipper.test.ts index f9c20e85eab..2d70ede3530 100644 --- a/src/test/abilities/sap_sipper.test.ts +++ b/src/test/abilities/sap_sipper.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { TerrainType } from "#app/data/terrain"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; @@ -9,6 +9,7 @@ import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { SPLASH_ONLY } from "../utils/testUtils"; // See also: TypeImmunityAbAttr describe("Abilities - Sap Sipper", () => { @@ -31,52 +32,55 @@ describe("Abilities - Sap Sipper", () => { game.override.disableCrits(); }); - it("raise attack 1 level and block effects when activated against a grass attack", async () => { + it("raises ATK stat stage by 1 and block effects when activated against a grass attack", async() => { const moveToUse = Moves.LEAFAGE; const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([moveToUse]); - game.override.enemyMoveset([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]); + game.override.moveset([ moveToUse ]); + game.override.enemyMoveset(SPLASH_ONLY); game.override.enemySpecies(Species.DUSKULL); game.override.enemyAbility(enemyAbility); await game.startBattle(); - const startingOppHp = game.scene.currentBattle.enemyParty[0].hp; + const enemyPokemon = game.scene.getEnemyPokemon()!; + const initialEnemyHp = enemyPokemon.hp; game.move.select(moveToUse); await game.phaseInterceptor.to(TurnEndPhase); - expect(startingOppHp - game.scene.getEnemyParty()[0].hp).toBe(0); - expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(1); + expect(initialEnemyHp - enemyPokemon.hp).toBe(0); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); }); - it("raise attack 1 level and block effects when activated against a grass status move", async () => { + it("raises ATK stat stage by 1 and block effects when activated against a grass status move", async() => { const moveToUse = Moves.SPORE; const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([moveToUse]); - game.override.enemyMoveset([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]); + game.override.moveset([ moveToUse ]); + game.override.enemyMoveset(SPLASH_ONLY); game.override.enemySpecies(Species.RATTATA); game.override.enemyAbility(enemyAbility); await game.startBattle(); + const enemyPokemon = game.scene.getEnemyPokemon()!; + game.move.select(moveToUse); await game.phaseInterceptor.to(TurnEndPhase); - expect(game.scene.getEnemyParty()[0].status).toBeUndefined(); - expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(1); + expect(enemyPokemon.status).toBeUndefined(); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); }); it("do not activate against status moves that target the field", async () => { const moveToUse = Moves.GRASSY_TERRAIN; const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([moveToUse]); - game.override.enemyMoveset([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]); + game.override.moveset([ moveToUse ]); + game.override.enemyMoveset(SPLASH_ONLY); game.override.enemySpecies(Species.RATTATA); game.override.enemyAbility(enemyAbility); @@ -88,51 +92,54 @@ describe("Abilities - Sap Sipper", () => { expect(game.scene.arena.terrain).toBeDefined(); expect(game.scene.arena.terrain!.terrainType).toBe(TerrainType.GRASSY); - expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(0); + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(0); }); it("activate once against multi-hit grass attacks", async () => { const moveToUse = Moves.BULLET_SEED; const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([moveToUse]); - game.override.enemyMoveset([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]); + game.override.moveset([ moveToUse ]); + game.override.enemyMoveset(SPLASH_ONLY); game.override.enemySpecies(Species.RATTATA); game.override.enemyAbility(enemyAbility); await game.startBattle(); - const startingOppHp = game.scene.currentBattle.enemyParty[0].hp; + const enemyPokemon = game.scene.getEnemyPokemon()!; + const initialEnemyHp = enemyPokemon.hp; game.move.select(moveToUse); await game.phaseInterceptor.to(TurnEndPhase); - expect(startingOppHp - game.scene.getEnemyParty()[0].hp).toBe(0); - expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(1); + expect(initialEnemyHp - enemyPokemon.hp).toBe(0); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); }); it("do not activate against status moves that target the user", async () => { const moveToUse = Moves.SPIKY_SHIELD; const ability = Abilities.SAP_SIPPER; - game.override.moveset([moveToUse]); + game.override.moveset([ moveToUse ]); game.override.ability(ability); - game.override.enemyMoveset([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]); + game.override.enemyMoveset(SPLASH_ONLY); game.override.enemySpecies(Species.RATTATA); game.override.enemyAbility(Abilities.NONE); await game.startBattle(); + const playerPokemon = game.scene.getPlayerPokemon()!; + game.move.select(moveToUse); await game.phaseInterceptor.to(MoveEndPhase); - expect(game.scene.getParty()[0].getTag(BattlerTagType.SPIKY_SHIELD)).toBeDefined(); + expect(playerPokemon.getTag(BattlerTagType.SPIKY_SHIELD)).toBeDefined(); await game.phaseInterceptor.to(TurnEndPhase); - expect(game.scene.getParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(0); + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); }); @@ -149,13 +156,14 @@ describe("Abilities - Sap Sipper", () => { await game.startBattle(); - const startingOppHp = game.scene.currentBattle.enemyParty[0].hp; + const enemyPokemon = game.scene.getEnemyPokemon()!; + const initialEnemyHp = enemyPokemon.hp; game.move.select(moveToUse); await game.phaseInterceptor.to(TurnEndPhase); - expect(startingOppHp - game.scene.getEnemyParty()[0].hp).toBe(0); - expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(1); + expect(initialEnemyHp - enemyPokemon.hp).toBe(0); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); }); }); diff --git a/src/test/abilities/serene_grace.test.ts b/src/test/abilities/serene_grace.test.ts index 7316b2ea920..e06288b9de9 100644 --- a/src/test/abilities/serene_grace.test.ts +++ b/src/test/abilities/serene_grace.test.ts @@ -1,6 +1,6 @@ import { BattlerIndex } from "#app/battle"; import { applyAbAttrs, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability"; -import { Stat } from "#app/data/pokemon-stat"; +import { Stat } from "#enums/stat"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import * as Utils from "#app/utils"; import { Abilities } from "#enums/abilities"; diff --git a/src/test/abilities/sheer_force.test.ts b/src/test/abilities/sheer_force.test.ts index f73b749dac2..69b47e1eaae 100644 --- a/src/test/abilities/sheer_force.test.ts +++ b/src/test/abilities/sheer_force.test.ts @@ -1,6 +1,6 @@ import { BattlerIndex } from "#app/battle"; import { applyAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr, PostDefendTypeChangeAbAttr } from "#app/data/ability"; -import { Stat } from "#app/data/pokemon-stat"; +import { Stat } from "#enums/stat"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import * as Utils from "#app/utils"; import { Abilities } from "#enums/abilities"; diff --git a/src/test/abilities/shield_dust.test.ts b/src/test/abilities/shield_dust.test.ts index 14770c49427..8a0b769827d 100644 --- a/src/test/abilities/shield_dust.test.ts +++ b/src/test/abilities/shield_dust.test.ts @@ -1,6 +1,6 @@ import { BattlerIndex } from "#app/battle"; import { applyAbAttrs, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability"; -import { Stat } from "#app/data/pokemon-stat"; +import { Stat } from "#enums/stat"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import * as Utils from "#app/utils"; import { Abilities } from "#enums/abilities"; diff --git a/src/test/abilities/simple.test.ts b/src/test/abilities/simple.test.ts new file mode 100644 index 00000000000..4310c5d45d1 --- /dev/null +++ b/src/test/abilities/simple.test.ts @@ -0,0 +1,42 @@ +import { Stat } from "#enums/stat"; +import GameManager from "#test/utils/gameManager"; +import { Abilities } from "#enums/abilities"; +import { Species } from "#enums/species"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { SPLASH_ONLY } from "../utils/testUtils"; + +describe("Abilities - Simple", () => { + 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 + .battleType("single") + .enemySpecies(Species.BULBASAUR) + .enemyAbility(Abilities.SIMPLE) + .ability(Abilities.INTIMIDATE) + .enemyMoveset(SPLASH_ONLY); + }); + + it("should double stat changes when applied", async() => { + await game.startBattle([ + Species.SLOWBRO + ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2); + }, 20000); +}); diff --git a/src/test/abilities/volt_absorb.test.ts b/src/test/abilities/volt_absorb.test.ts index d9c3fe34c24..7f3e160c7d0 100644 --- a/src/test/abilities/volt_absorb.test.ts +++ b/src/test/abilities/volt_absorb.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -41,12 +41,14 @@ describe("Abilities - Volt Absorb", () => { await game.startBattle(); + const playerPokemon = game.scene.getPlayerPokemon()!; + game.move.select(moveToUse); await game.phaseInterceptor.to(TurnEndPhase); - expect(game.scene.getParty()[0].summonData.battleStats[BattleStat.SPDEF]).toBe(1); - expect(game.scene.getParty()[0].getTag(BattlerTagType.CHARGED)).toBeDefined(); + expect(playerPokemon.getStatStage(Stat.SPDEF)).toBe(1); + expect(playerPokemon.getTag(BattlerTagType.CHARGED)).toBeDefined(); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); }); }); diff --git a/src/test/abilities/wind_rider.test.ts b/src/test/abilities/wind_rider.test.ts index e11b3b39723..7a1fee6794a 100644 --- a/src/test/abilities/wind_rider.test.ts +++ b/src/test/abilities/wind_rider.test.ts @@ -1,8 +1,8 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; +import GameManager from "#test/utils/gameManager"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import GameManager from "#test/utils/gameManager"; import { SPLASH_ONLY } from "#test/utils/testUtils"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -31,56 +31,38 @@ describe("Abilities - Wind Rider", () => { .enemyMoveset(SPLASH_ONLY); }); - it("takes no damage from wind moves and its Attack is increased by one stage when hit by one", async () => { - await game.classicMode.startBattle([Species.MAGIKARP]); + it("takes no damage from wind moves and its ATK stat stage is raised by 1 when hit by one", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); const shiftry = game.scene.getEnemyPokemon()!; - expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0); + expect(shiftry.getStatStage(Stat.ATK)).toBe(0); game.move.select(Moves.PETAL_BLIZZARD); await game.phaseInterceptor.to("TurnEndPhase"); expect(shiftry.isFullHp()).toBe(true); - expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(1); + expect(shiftry.getStatStage(Stat.ATK)).toBe(1); }); - it("Attack is increased by one stage when Tailwind is present on its side", async () => { - game.override.ability(Abilities.WIND_RIDER); - game.override.enemySpecies(Species.MAGIKARP); + it("ATK stat stage is raised by 1 when Tailwind is present on its side", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .ability(Abilities.WIND_RIDER); await game.classicMode.startBattle([Species.SHIFTRY]); const shiftry = game.scene.getPlayerPokemon()!; - expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0); + expect(shiftry.getStatStage(Stat.ATK)).toBe(0); game.move.select(Moves.TAILWIND); await game.phaseInterceptor.to("TurnEndPhase"); - expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(1); + expect(shiftry.getStatStage(Stat.ATK)).toBe(1); }); - it("does not increase Attack when Tailwind is present on opposing side", async () => { - game.override.ability(Abilities.WIND_RIDER); - game.override.enemySpecies(Species.MAGIKARP); - - await game.classicMode.startBattle([Species.SHIFTRY]); - const magikarp = game.scene.getEnemyPokemon()!; - const shiftry = game.scene.getPlayerPokemon()!; - - expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0); - expect(magikarp.summonData.battleStats[BattleStat.ATK]).toBe(0); - - game.move.select(Moves.TAILWIND); - - await game.phaseInterceptor.to("TurnEndPhase"); - - expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(1); - expect(magikarp.summonData.battleStats[BattleStat.ATK]).toBe(0); - }); - - it("does not increase Attack when Tailwind is present on opposing side", async () => { + it("does not raise ATK stat stage when Tailwind is present on opposing side", async () => { game.override .enemySpecies(Species.MAGIKARP) .ability(Abilities.WIND_RIDER); @@ -89,15 +71,35 @@ describe("Abilities - Wind Rider", () => { const magikarp = game.scene.getEnemyPokemon()!; const shiftry = game.scene.getPlayerPokemon()!; - expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0); - expect(magikarp.summonData.battleStats[BattleStat.ATK]).toBe(0); + expect(shiftry.getStatStage(Stat.ATK)).toBe(0); + expect(magikarp.getStatStage(Stat.ATK)).toBe(0); game.move.select(Moves.TAILWIND); await game.phaseInterceptor.to("TurnEndPhase"); - expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(1); - expect(magikarp.summonData.battleStats[BattleStat.ATK]).toBe(0); + expect(shiftry.getStatStage(Stat.ATK)).toBe(1); + expect(magikarp.getStatStage(Stat.ATK)).toBe(0); + }); + + it("does not raise ATK stat stage when Tailwind is present on opposing side", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .ability(Abilities.WIND_RIDER); + + await game.classicMode.startBattle([Species.SHIFTRY]); + const magikarp = game.scene.getEnemyPokemon()!; + const shiftry = game.scene.getPlayerPokemon()!; + + expect(shiftry.getStatStage(Stat.ATK)).toBe(0); + expect(magikarp.getStatStage(Stat.ATK)).toBe(0); + + game.move.select(Moves.TAILWIND); + + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(shiftry.getStatStage(Stat.ATK)).toBe(1); + expect(magikarp.getStatStage(Stat.ATK)).toBe(0); }); it("does not interact with Sandstorm", async () => { @@ -106,14 +108,14 @@ describe("Abilities - Wind Rider", () => { await game.classicMode.startBattle([Species.SHIFTRY]); const shiftry = game.scene.getPlayerPokemon()!; - expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0); + expect(shiftry.getStatStage(Stat.ATK)).toBe(0); expect(shiftry.isFullHp()).toBe(true); game.move.select(Moves.SANDSTORM); await game.phaseInterceptor.to("TurnEndPhase"); - expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0); + expect(shiftry.getStatStage(Stat.ATK)).toBe(0); expect(shiftry.hp).lessThan(shiftry.getMaxHp()); }); }); diff --git a/src/test/abilities/zen_mode.test.ts b/src/test/abilities/zen_mode.test.ts index 677d998e876..fd378647184 100644 --- a/src/test/abilities/zen_mode.test.ts +++ b/src/test/abilities/zen_mode.test.ts @@ -1,6 +1,5 @@ +import { Stat } from "#enums/stat"; import { BattlerIndex } from "#app/battle"; -import { Stat } from "#app/data/pokemon-stat"; -import { Status, StatusEffect } from "#app/data/status-effect"; import { DamagePhase } from "#app/phases/damage-phase"; import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; import { MessagePhase } from "#app/phases/message-phase"; @@ -18,6 +17,7 @@ import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; +import { Status, StatusEffect } from "#app/data/status-effect"; const TIMEOUT = 20 * 1000; diff --git a/src/test/achievements/achievement.test.ts b/src/test/achievements/achievement.test.ts index 36c20ae2248..24d00a3e77b 100644 --- a/src/test/achievements/achievement.test.ts +++ b/src/test/achievements/achievement.test.ts @@ -224,7 +224,7 @@ describe("achvs", () => { expect(achvs._50_RIBBONS).toBeInstanceOf(RibbonAchv); expect(achvs._75_RIBBONS).toBeInstanceOf(RibbonAchv); expect(achvs._100_RIBBONS).toBeInstanceOf(RibbonAchv); - expect(achvs.TRANSFER_MAX_BATTLE_STAT).toBeInstanceOf(Achv); + expect(achvs.TRANSFER_MAX_STAT_STAGE).toBeInstanceOf(Achv); expect(achvs.MAX_FRIENDSHIP).toBeInstanceOf(Achv); expect(achvs.MEGA_EVOLVE).toBeInstanceOf(Achv); expect(achvs.GIGANTAMAX).toBeInstanceOf(Achv); diff --git a/src/test/battle-stat.spec.ts b/src/test/battle-stat.spec.ts deleted file mode 100644 index 16fce962838..00000000000 --- a/src/test/battle-stat.spec.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "#app/data/battle-stat"; -import { describe, expect, it } from "vitest"; -import { arrayOfRange, mockI18next } from "./utils/testUtils"; - -const TEST_BATTLE_STAT = -99 as unknown as BattleStat; -const TEST_POKEMON = "Testmon"; -const TEST_STAT = "Teststat"; - -describe("battle-stat", () => { - describe("getBattleStatName", () => { - it("should return the correct name for each BattleStat", () => { - mockI18next(); - - expect(getBattleStatName(BattleStat.ATK)).toBe("pokemonInfo:Stat.ATK"); - expect(getBattleStatName(BattleStat.DEF)).toBe("pokemonInfo:Stat.DEF"); - expect(getBattleStatName(BattleStat.SPATK)).toBe( - "pokemonInfo:Stat.SPATK" - ); - expect(getBattleStatName(BattleStat.SPDEF)).toBe( - "pokemonInfo:Stat.SPDEF" - ); - expect(getBattleStatName(BattleStat.SPD)).toBe("pokemonInfo:Stat.SPD"); - expect(getBattleStatName(BattleStat.ACC)).toBe("pokemonInfo:Stat.ACC"); - expect(getBattleStatName(BattleStat.EVA)).toBe("pokemonInfo:Stat.EVA"); - }); - - it("should fall back to ??? for an unknown BattleStat", () => { - expect(getBattleStatName(TEST_BATTLE_STAT)).toBe("???"); - }); - }); - - describe("getBattleStatLevelChangeDescription", () => { - it("should return battle:statRose for +1", () => { - mockI18next(); - - const message = getBattleStatLevelChangeDescription( - TEST_POKEMON, - TEST_STAT, - 1, - true - ); - - expect(message).toBe("battle:statRose"); - }); - - it("should return battle:statSharplyRose for +2", () => { - mockI18next(); - - const message = getBattleStatLevelChangeDescription( - TEST_POKEMON, - TEST_STAT, - 2, - true - ); - - expect(message).toBe("battle:statSharplyRose"); - }); - - it("should return battle:statRoseDrastically for +3 to +6", () => { - mockI18next(); - - arrayOfRange(3, 6).forEach((n) => { - const message = getBattleStatLevelChangeDescription( - TEST_POKEMON, - TEST_STAT, - n, - true - ); - - expect(message).toBe("battle:statRoseDrastically"); - }); - }); - - it("should return battle:statWontGoAnyHigher for 7 or higher", () => { - mockI18next(); - - arrayOfRange(7, 10).forEach((n) => { - const message = getBattleStatLevelChangeDescription( - TEST_POKEMON, - TEST_STAT, - n, - true - ); - - expect(message).toBe("battle:statWontGoAnyHigher"); - }); - }); - - it("should return battle:statFell for -1", () => { - mockI18next(); - - const message = getBattleStatLevelChangeDescription( - TEST_POKEMON, - TEST_STAT, - 1, - false - ); - - expect(message).toBe("battle:statFell"); - }); - - it("should return battle:statHarshlyFell for -2", () => { - mockI18next(); - - const message = getBattleStatLevelChangeDescription( - TEST_POKEMON, - TEST_STAT, - 2, - false - ); - - expect(message).toBe("battle:statHarshlyFell"); - }); - - it("should return battle:statSeverelyFell for -3 to -6", () => { - mockI18next(); - - arrayOfRange(3, 6).forEach((n) => { - const message = getBattleStatLevelChangeDescription( - TEST_POKEMON, - TEST_STAT, - n, - false - ); - - expect(message).toBe("battle:statSeverelyFell"); - }); - }); - - it("should return battle:statWontGoAnyLower for -7 or lower", () => { - mockI18next(); - - arrayOfRange(7, 10).forEach((n) => { - const message = getBattleStatLevelChangeDescription( - TEST_POKEMON, - TEST_STAT, - n, - false - ); - - expect(message).toBe("battle:statWontGoAnyLower"); - }); - }); - }); -}); diff --git a/src/test/battle/battle.test.ts b/src/test/battle/battle.test.ts index be89fdeb2af..25dfbc765bd 100644 --- a/src/test/battle/battle.test.ts +++ b/src/test/battle/battle.test.ts @@ -1,5 +1,5 @@ import { allSpecies } from "#app/data/pokemon-species"; -import { TempBattleStat } from "#app/data/temp-battle-stat"; +import { Stat } from "#enums/stat"; import { GameModes, getGameMode } from "#app/game-mode"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { CommandPhase } from "#app/phases/command-phase"; @@ -320,7 +320,7 @@ describe("Test Battle Phase", () => { .startingLevel(100) .moveset([moveToUse]) .enemyMoveset(SPLASH_ONLY) - .startingHeldItems([{ name: "TEMP_STAT_BOOSTER", type: TempBattleStat.ACC }]); + .startingHeldItems([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }]); await game.startBattle(); game.scene.getPlayerPokemon()!.hp = 1; diff --git a/src/test/battlerTags/octolock.test.ts b/src/test/battlerTags/octolock.test.ts index fa491589f09..7b1f9264370 100644 --- a/src/test/battlerTags/octolock.test.ts +++ b/src/test/battlerTags/octolock.test.ts @@ -1,16 +1,16 @@ import BattleScene from "#app/battle-scene"; -import { BattleStat } from "#app/data/battle-stat"; -import { BattlerTag, BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; -import Pokemon from "#app/field/pokemon"; -import { StatChangePhase } from "#app/phases/stat-change-phase"; import { describe, expect, it, vi } from "vitest"; +import Pokemon from "#app/field/pokemon"; +import { BattlerTag, BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { Stat } from "#enums/stat"; vi.mock("#app/battle-scene.js"); describe("BattlerTag - OctolockTag", () => { describe("lapse behavior", () => { - it("unshifts a StatChangePhase with expected stat changes", { timeout: 10000 }, async () => { + it("unshifts a StatStageChangePhase with expected stat stage changes", { timeout: 10000 }, async () => { const mockPokemon = { scene: new BattleScene(), getBattlerIndex: () => 0, @@ -19,9 +19,9 @@ describe("BattlerTag - OctolockTag", () => { const subject = new OctolockTag(1); vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => { - expect(phase).toBeInstanceOf(StatChangePhase); - expect((phase as StatChangePhase)["levels"]).toEqual(-1); - expect((phase as StatChangePhase)["stats"]).toEqual([BattleStat.DEF, BattleStat.SPDEF]); + expect(phase).toBeInstanceOf(StatStageChangePhase); + expect((phase as StatStageChangePhase)["stages"]).toEqual(-1); + expect((phase as StatStageChangePhase)["stats"]).toEqual([ Stat.DEF, Stat.SPDEF ]); }); subject.lapse(mockPokemon, BattlerTagLapseType.TURN_END); diff --git a/src/test/battlerTags/stockpiling.test.ts b/src/test/battlerTags/stockpiling.test.ts index fef1e938c09..e568016dfef 100644 --- a/src/test/battlerTags/stockpiling.test.ts +++ b/src/test/battlerTags/stockpiling.test.ts @@ -1,10 +1,10 @@ import BattleScene from "#app/battle-scene"; -import { BattleStat } from "#app/data/battle-stat"; -import { StockpilingTag } from "#app/data/battler-tags"; -import Pokemon, { PokemonSummonData } from "#app/field/pokemon"; -import * as messages from "#app/messages"; -import { StatChangePhase } from "#app/phases/stat-change-phase"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import Pokemon, { PokemonSummonData } from "#app/field/pokemon"; +import { StockpilingTag } from "#app/data/battler-tags"; +import { Stat } from "#enums/stat"; +import * as messages from "#app/messages"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; beforeEach(() => { vi.spyOn(messages, "getPokemonNameWithAffix").mockImplementation(() => ""); @@ -12,7 +12,7 @@ beforeEach(() => { describe("BattlerTag - StockpilingTag", () => { describe("onAdd", () => { - it("unshifts a StatChangePhase with expected stat changes on add", { timeout: 10000 }, async () => { + it("unshifts a StatStageChangePhase with expected stat stage changes on add", { timeout: 10000 }, async () => { const mockPokemon = { scene: vi.mocked(new BattleScene()) as BattleScene, getBattlerIndex: () => 0, @@ -23,11 +23,11 @@ describe("BattlerTag - StockpilingTag", () => { const subject = new StockpilingTag(1); vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => { - expect(phase).toBeInstanceOf(StatChangePhase); - expect((phase as StatChangePhase)["levels"]).toEqual(1); - expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF])); + expect(phase).toBeInstanceOf(StatStageChangePhase); + expect((phase as StatStageChangePhase)["stages"]).toEqual(1); + expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ])); - (phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.DEF, BattleStat.SPDEF], [1, 1]); + (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [Stat.DEF, Stat.SPDEF], [1, 1]); }); subject.onAdd(mockPokemon); @@ -35,7 +35,7 @@ describe("BattlerTag - StockpilingTag", () => { expect(mockPokemon.scene.unshiftPhase).toBeCalledTimes(1); }); - it("unshifts a StatChangePhase with expected stat changes on add (one stat maxed)", { timeout: 10000 }, async () => { + it("unshifts a StatStageChangePhase with expected stat changes on add (one stat maxed)", { timeout: 10000 }, async () => { const mockPokemon = { scene: new BattleScene(), summonData: new PokemonSummonData(), @@ -44,17 +44,17 @@ describe("BattlerTag - StockpilingTag", () => { vi.spyOn(mockPokemon.scene, "queueMessage").mockImplementation(() => {}); - mockPokemon.summonData.battleStats[BattleStat.DEF] = 6; - mockPokemon.summonData.battleStats[BattleStat.SPDEF] = 5; + mockPokemon.summonData.statStages[Stat.DEF - 1] = 6; + mockPokemon.summonData.statStages[Stat.SPD - 1] = 5; const subject = new StockpilingTag(1); vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => { - expect(phase).toBeInstanceOf(StatChangePhase); - expect((phase as StatChangePhase)["levels"]).toEqual(1); - expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF])); + expect(phase).toBeInstanceOf(StatStageChangePhase); + expect((phase as StatStageChangePhase)["stages"]).toEqual(1); + expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([Stat.DEF, Stat.SPDEF])); - (phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.DEF, BattleStat.SPDEF], [1, 1]); + (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [ Stat.DEF, Stat.SPDEF ], [1, 1]); }); subject.onAdd(mockPokemon); @@ -64,7 +64,7 @@ describe("BattlerTag - StockpilingTag", () => { }); describe("onOverlap", () => { - it("unshifts a StatChangePhase with expected stat changes on overlap", { timeout: 10000 }, async () => { + it("unshifts a StatStageChangePhase with expected stat changes on overlap", { timeout: 10000 }, async () => { const mockPokemon = { scene: new BattleScene(), getBattlerIndex: () => 0, @@ -75,11 +75,11 @@ describe("BattlerTag - StockpilingTag", () => { const subject = new StockpilingTag(1); vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => { - expect(phase).toBeInstanceOf(StatChangePhase); - expect((phase as StatChangePhase)["levels"]).toEqual(1); - expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF])); + expect(phase).toBeInstanceOf(StatStageChangePhase); + expect((phase as StatStageChangePhase)["stages"]).toEqual(1); + expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ])); - (phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.DEF, BattleStat.SPDEF], [1, 1]); + (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [ Stat.DEF, Stat.SPDEF ], [1, 1]); }); subject.onOverlap(mockPokemon); @@ -98,39 +98,39 @@ describe("BattlerTag - StockpilingTag", () => { vi.spyOn(mockPokemon.scene, "queueMessage").mockImplementation(() => {}); - mockPokemon.summonData.battleStats[BattleStat.DEF] = 5; - mockPokemon.summonData.battleStats[BattleStat.SPDEF] = 4; + mockPokemon.summonData.statStages[Stat.DEF - 1] = 5; + mockPokemon.summonData.statStages[Stat.SPD - 1] = 4; const subject = new StockpilingTag(1); vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => { - expect(phase).toBeInstanceOf(StatChangePhase); - expect((phase as StatChangePhase)["levels"]).toEqual(1); - expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF])); + expect(phase).toBeInstanceOf(StatStageChangePhase); + expect((phase as StatStageChangePhase)["stages"]).toEqual(1); + expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ])); // def doesn't change - (phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.SPDEF], [1]); + (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [ Stat.SPDEF ], [1]); }); subject.onAdd(mockPokemon); expect(subject.stockpiledCount).toBe(1); vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => { - expect(phase).toBeInstanceOf(StatChangePhase); - expect((phase as StatChangePhase)["levels"]).toEqual(1); - expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF])); + expect(phase).toBeInstanceOf(StatStageChangePhase); + expect((phase as StatStageChangePhase)["stages"]).toEqual(1); + expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ])); // def doesn't change - (phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.SPDEF], [1]); + (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [ Stat.SPDEF ], [1]); }); subject.onOverlap(mockPokemon); expect(subject.stockpiledCount).toBe(2); vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => { - expect(phase).toBeInstanceOf(StatChangePhase); - expect((phase as StatChangePhase)["levels"]).toEqual(1); - expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF])); + expect(phase).toBeInstanceOf(StatStageChangePhase); + expect((phase as StatStageChangePhase)["stages"]).toEqual(1); + expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ])); // neither stat changes, stack count should still increase }); @@ -138,20 +138,20 @@ describe("BattlerTag - StockpilingTag", () => { subject.onOverlap(mockPokemon); expect(subject.stockpiledCount).toBe(3); - vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => { + vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(_phase => { throw new Error("Should not be called a fourth time"); }); // fourth stack should not be applied subject.onOverlap(mockPokemon); expect(subject.stockpiledCount).toBe(3); - expect(subject.statChangeCounts).toMatchObject({ [BattleStat.DEF]: 0, [BattleStat.SPDEF]: 2 }); + expect(subject.statChangeCounts).toMatchObject({ [ Stat.DEF ]: 0, [Stat.SPDEF]: 2 }); // removing tag should reverse stat changes vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => { - expect(phase).toBeInstanceOf(StatChangePhase); - expect((phase as StatChangePhase)["levels"]).toEqual(-2); - expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.SPDEF])); + expect(phase).toBeInstanceOf(StatStageChangePhase); + expect((phase as StatStageChangePhase)["stages"]).toEqual(-2); + expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([Stat.SPDEF])); }); subject.onRemove(mockPokemon); diff --git a/src/test/boss-pokemon.test.ts b/src/test/boss-pokemon.test.ts index 3e6701c7e4f..f8437932580 100644 --- a/src/test/boss-pokemon.test.ts +++ b/src/test/boss-pokemon.test.ts @@ -5,7 +5,7 @@ import { getPokemonSpecies } from "#app/data/pokemon-species"; import { SPLASH_ONLY } from "./utils/testUtils"; import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; -import { BattleStat } from "#app/data/battle-stat"; +import { EFFECTIVE_STATS } from "#app/enums/stat"; import { EnemyPokemon } from "#app/field/pokemon"; import { toDmgValue } from "#app/utils"; @@ -80,7 +80,7 @@ describe("Boss Pokemon / Shields", () => { expect(boss2.bossSegments).toBe(2); }, TIMEOUT); - it("shields should stop overflow damage and give stat boosts when broken", async () => { + it("shields should stop overflow damage and give stat stage boosts when broken", async () => { game.override.startingWave(150); // Floor 150 > 2 shields / 3 health segments await game.classicMode.startBattle([ Species.MEWTWO ]); @@ -89,7 +89,7 @@ describe("Boss Pokemon / Shields", () => { const segmentHp = enemyPokemon.getMaxHp() / enemyPokemon.bossSegments; expect(enemyPokemon.isBoss()).toBe(true); expect(enemyPokemon.bossSegments).toBe(3); - expect(getTotalStatBoosts(enemyPokemon)).toBe(0); + expect(getTotalStatStageBoosts(enemyPokemon)).toBe(0); game.move.select(Moves.SUPER_FANG); // Enough to break the first shield await game.toNextTurn(); @@ -98,7 +98,7 @@ describe("Boss Pokemon / Shields", () => { expect(enemyPokemon.bossSegmentIndex).toBe(1); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - toDmgValue(segmentHp)); // Breaking the shield gives a +1 boost to ATK, DEF, SP ATK, SP DEF or SPD - expect(getTotalStatBoosts(enemyPokemon)).toBe(1); + expect(getTotalStatStageBoosts(enemyPokemon)).toBe(1); game.move.select(Moves.FALSE_SWIPE); // Enough to break last shield but not kill await game.toNextTurn(); @@ -106,7 +106,7 @@ describe("Boss Pokemon / Shields", () => { expect(enemyPokemon.bossSegmentIndex).toBe(0); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - toDmgValue(2 * segmentHp)); // Breaking the last shield gives a +2 boost to ATK, DEF, SP ATK, SP DEF or SPD - expect(getTotalStatBoosts(enemyPokemon)).toBe(3); + expect(getTotalStatStageBoosts(enemyPokemon)).toBe(3); }, TIMEOUT); @@ -146,7 +146,7 @@ describe("Boss Pokemon / Shields", () => { }, TIMEOUT); - it("the number of stats boosts is consistent when several shields are broken at once", async () => { + it("the number of stat stage boosts is consistent when several shields are broken at once", async () => { const shieldsToBreak = 4; game.override @@ -161,22 +161,22 @@ describe("Boss Pokemon / Shields", () => { expect(boss1.isBoss()).toBe(true); expect(boss1.bossSegments).toBe(shieldsToBreak + 1); expect(boss1.bossSegmentIndex).toBe(shieldsToBreak); - expect(getTotalStatBoosts(boss1)).toBe(0); + expect(getTotalStatStageBoosts(boss1)).toBe(0); - let totalStats = 0; + let totalStatStages = 0; // Break the shields one by one for (let i = 1; i <= shieldsToBreak; i++) { boss1.damageAndUpdate(singleShieldDamage); expect(boss1.bossSegmentIndex).toBe(shieldsToBreak - i); expect(boss1.hp).toBe(boss1.getMaxHp() - toDmgValue(boss1SegmentHp * i)); - // Do nothing and go to next turn so that the StatChangePhase gets applied + // Do nothing and go to next turn so that the StatStageChangePhase gets applied game.move.select(Moves.SPLASH); await game.toNextTurn(); // All broken shields give +1 stat boost, except the last two that gives +2 - totalStats += i >= shieldsToBreak -1? 2 : 1; - expect(getTotalStatBoosts(boss1)).toBe(totalStats); + totalStatStages += i >= shieldsToBreak -1? 2 : 1; + expect(getTotalStatStageBoosts(boss1)).toBe(totalStatStages); } const boss2: EnemyPokemon = game.scene.getEnemyParty()[1]!; @@ -186,35 +186,30 @@ describe("Boss Pokemon / Shields", () => { expect(boss2.isBoss()).toBe(true); expect(boss2.bossSegments).toBe(shieldsToBreak + 1); expect(boss2.bossSegmentIndex).toBe(shieldsToBreak); - expect(getTotalStatBoosts(boss2)).toBe(0); + expect(getTotalStatStageBoosts(boss2)).toBe(0); // Enough damage to break all shields at once boss2.damageAndUpdate(Math.ceil(requiredDamage)); expect(boss2.bossSegmentIndex).toBe(0); expect(boss2.hp).toBe(boss2.getMaxHp() - toDmgValue(boss2SegmentHp * shieldsToBreak)); - // Do nothing and go to next turn so that the StatChangePhase gets applied + // Do nothing and go to next turn so that the StatStageChangePhase gets applied game.move.select(Moves.SPLASH); await game.toNextTurn(); - expect(getTotalStatBoosts(boss2)).toBe(totalStats); + expect(getTotalStatStageBoosts(boss2)).toBe(totalStatStages); }, TIMEOUT); /** - * Gets the sum of the ATK, DEF, SP ATK, SP DEF and SPD boosts for the given Pokemon + * Gets the sum of the effective stat stage boosts for the given Pokemon * @param enemyPokemon the pokemon to get stats from * @returns the total stats boosts */ - function getTotalStatBoosts(enemyPokemon: EnemyPokemon): number { - const enemyBattleStats = enemyPokemon.summonData.battleStats; - return enemyBattleStats?.reduce(statsSum, 0); - } - - function statsSum(total: number, value: number, index: number) { - if (index <= BattleStat.SPD) { - return total + value; + function getTotalStatStageBoosts(enemyPokemon: EnemyPokemon): number { + let boosts = 0; + for (const s of EFFECTIVE_STATS) { + boosts += enemyPokemon.getStatStage(s); } - return total; + return boosts; } - }); diff --git a/src/test/items/dire_hit.test.ts b/src/test/items/dire_hit.test.ts new file mode 100644 index 00000000000..c43091d1f03 --- /dev/null +++ b/src/test/items/dire_hit.test.ts @@ -0,0 +1,97 @@ +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phase from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { SPLASH_ONLY } from "../utils/testUtils"; +import { BattleEndPhase } from "#app/phases/battle-end-phase"; +import { TempCritBoosterModifier } from "#app/modifier/modifier"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { Button } from "#app/enums/buttons"; +import { CommandPhase } from "#app/phases/command-phase"; +import { NewBattlePhase } from "#app/phases/new-battle-phase"; +import { TurnInitPhase } from "#app/phases/turn-init-phase"; + +describe("Items - Dire Hit", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phase.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override + .enemySpecies(Species.MAGIKARP) + .enemyMoveset(SPLASH_ONLY) + .moveset([ Moves.POUND ]) + .startingHeldItems([{ name: "DIRE_HIT" }]) + .battleType("single") + .disableCrits(); + + }, 20000); + + it("should raise CRIT stage by 1", async () => { + await game.startBattle([ + Species.GASTLY + ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + vi.spyOn(enemyPokemon, "getCritStage"); + + game.move.select(Moves.POUND); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(enemyPokemon.getCritStage).toHaveReturnedWith(1); + }, 20000); + + it("should renew how many battles are left of existing DIRE_HIT when picking up new DIRE_HIT", async() => { + game.override.itemRewards([{ name: "DIRE_HIT" }]); + + await game.startBattle([ + Species.PIKACHU + ]); + + game.move.select(Moves.SPLASH); + + await game.doKillOpponents(); + + await game.phaseInterceptor.to(BattleEndPhase); + + const modifier = game.scene.findModifier(m => m instanceof TempCritBoosterModifier) as TempCritBoosterModifier; + expect(modifier.getBattlesLeft()).toBe(4); + + // Forced DIRE_HIT to spawn in the first slot with override + game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => { + const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler; + // Traverse to first modifier slot + handler.processInput(Button.LEFT); + handler.processInput(Button.UP); + handler.processInput(Button.ACTION); + }, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(NewBattlePhase), true); + + await game.phaseInterceptor.to(TurnInitPhase); + + // Making sure only one booster is in the modifier list even after picking up another + let count = 0; + for (const m of game.scene.modifiers) { + if (m instanceof TempCritBoosterModifier) { + count++; + expect((m as TempCritBoosterModifier).getBattlesLeft()).toBe(5); + } + } + expect(count).toBe(1); + }, 20000); +}); diff --git a/src/test/items/eviolite.test.ts b/src/test/items/eviolite.test.ts index e491784acec..83b00583893 100644 --- a/src/test/items/eviolite.test.ts +++ b/src/test/items/eviolite.test.ts @@ -1,4 +1,4 @@ -import { Stat } from "#app/data/pokemon-stat"; +import { Stat } from "#enums/stat"; import { EvolutionStatBoosterModifier } from "#app/modifier/modifier"; import { modifierTypes } from "#app/modifier/modifier-type"; import i18next from "#app/plugins/i18n"; @@ -37,29 +37,29 @@ describe("Items - Eviolite", () => { const partyMember = game.scene.getParty()[0]; - // Checking consoe log to make sure Eviolite is applied when getBattleStat (with the appropriate stat) is called - partyMember.getBattleStat(Stat.DEF); + // Checking console log to make sure Eviolite is applied when getEffectiveStat (with the appropriate stat) is called + partyMember.getEffectiveStat(Stat.DEF); expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), ""); // Printing dummy console messages along the way so subsequent checks don't pass because of the first console.log(""); - partyMember.getBattleStat(Stat.SPDEF); + partyMember.getEffectiveStat(Stat.SPDEF); expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), ""); console.log(""); - partyMember.getBattleStat(Stat.ATK); + partyMember.getEffectiveStat(Stat.ATK); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), ""); console.log(""); - partyMember.getBattleStat(Stat.SPATK); + partyMember.getEffectiveStat(Stat.SPATK); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), ""); console.log(""); - partyMember.getBattleStat(Stat.SPD); + partyMember.getEffectiveStat(Stat.SPD); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), ""); }); diff --git a/src/test/items/leek.test.ts b/src/test/items/leek.test.ts index 7505b6374a0..af20516ef83 100644 --- a/src/test/items/leek.test.ts +++ b/src/test/items/leek.test.ts @@ -1,7 +1,4 @@ -import { BattlerIndex } from "#app/battle"; -import { CritBoosterModifier } from "#app/modifier/modifier"; -import { modifierTypes } from "#app/modifier/modifier-type"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; import * as Utils from "#app/utils"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -26,91 +23,64 @@ describe("Items - Leek", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.enemySpecies(Species.MAGIKARP); - game.override.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); - game.override.disableCrits(); - - game.override.battleType("single"); + game.override + .enemySpecies(Species.MAGIKARP) + .enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]) + .startingHeldItems([{ name: "LEEK" }]) + .moveset([ Moves.TACKLE ]) + .disableCrits() + .battleType("single"); }); - it("LEEK activates in battle correctly", async () => { - game.override.startingHeldItems([{ name: "LEEK" }]); - game.override.moveset([Moves.POUND]); - const consoleSpy = vi.spyOn(console, "log"); + it("should raise CRIT stage by 2 when held by FARFETCHD", async () => { await game.startBattle([ Species.FARFETCHD ]); - game.move.select(Moves.POUND); + const enemyMember = game.scene.getEnemyPokemon()!; - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + vi.spyOn(enemyMember, "getCritStage"); - await game.phaseInterceptor.to(MoveEffectPhase); + game.move.select(Moves.TACKLE); - expect(consoleSpy).toHaveBeenCalledWith("Applied", "Leek", ""); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(enemyMember.getCritStage).toHaveReturnedWith(2); }, 20000); - it("LEEK held by FARFETCHD", async () => { - await game.startBattle([ - Species.FARFETCHD - ]); - - const partyMember = game.scene.getPlayerPokemon()!; - - // Making sure modifier is not applied without holding item - const critLevel = new Utils.IntegerHolder(0); - partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel); - - expect(critLevel.value).toBe(0); - - // Giving Leek to party member and testing if it applies - partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true); - partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel); - - expect(critLevel.value).toBe(2); - }, 20000); - - it("LEEK held by GALAR_FARFETCHD", async () => { + it("should raise CRIT stage by 2 when held by GALAR_FARFETCHD", async () => { await game.startBattle([ Species.GALAR_FARFETCHD ]); - const partyMember = game.scene.getPlayerPokemon()!; + const enemyMember = game.scene.getEnemyPokemon()!; - // Making sure modifier is not applied without holding item - const critLevel = new Utils.IntegerHolder(0); - partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel); + vi.spyOn(enemyMember, "getCritStage"); - expect(critLevel.value).toBe(0); + game.move.select(Moves.TACKLE); - // Giving Leek to party member and testing if it applies - partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true); - partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel); + await game.phaseInterceptor.to(TurnEndPhase); - expect(critLevel.value).toBe(2); + expect(enemyMember.getCritStage).toHaveReturnedWith(2); }, 20000); - it("LEEK held by SIRFETCHD", async () => { + it("should raise CRIT stage by 2 when held by SIRFETCHD", async () => { await game.startBattle([ Species.SIRFETCHD ]); - const partyMember = game.scene.getPlayerPokemon()!; + const enemyMember = game.scene.getEnemyPokemon()!; - // Making sure modifier is not applied without holding item - const critLevel = new Utils.IntegerHolder(0); - partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel); + vi.spyOn(enemyMember, "getCritStage"); - expect(critLevel.value).toBe(0); + game.move.select(Moves.TACKLE); - // Giving Leek to party member and testing if it applies - partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true); - partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel); + await game.phaseInterceptor.to(TurnEndPhase); - expect(critLevel.value).toBe(2); + expect(enemyMember.getCritStage).toHaveReturnedWith(2); }, 20000); - it("LEEK held by fused FARFETCHD line (base)", async () => { + it("should raise CRIT stage by 2 when held by FARFETCHD line fused with Pokemon", async () => { // Randomly choose from the Farfetch'd line const species = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.SIRFETCHD]; @@ -119,9 +89,7 @@ describe("Items - Leek", () => { Species.PIKACHU, ]); - const party = game.scene.getParty(); - const partyMember = party[0]; - const ally = party[1]; + const [ partyMember, ally ] = game.scene.getParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -132,20 +100,18 @@ describe("Items - Leek", () => { partyMember.fusionGender = ally.gender; partyMember.fusionLuck = ally.luck; - // Making sure modifier is not applied without holding item - const critLevel = new Utils.IntegerHolder(0); - partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel); + const enemyMember = game.scene.getEnemyPokemon()!; - expect(critLevel.value).toBe(0); + vi.spyOn(enemyMember, "getCritStage"); - // Giving Leek to party member and testing if it applies - partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true); - partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel); + game.move.select(Moves.TACKLE); - expect(critLevel.value).toBe(2); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(enemyMember.getCritStage).toHaveReturnedWith(2); }, 20000); - it("LEEK held by fused FARFETCHD line (part)", async () => { + it("should raise CRIT stage by 2 when held by Pokemon fused with FARFETCHD line", async () => { // Randomly choose from the Farfetch'd line const species = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.SIRFETCHD]; @@ -154,9 +120,7 @@ describe("Items - Leek", () => { species[Utils.randInt(species.length)] ]); - const party = game.scene.getParty(); - const partyMember = party[0]; - const ally = party[1]; + const [ partyMember, ally ] = game.scene.getParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -167,36 +131,31 @@ describe("Items - Leek", () => { partyMember.fusionGender = ally.gender; partyMember.fusionLuck = ally.luck; - // Making sure modifier is not applied without holding item - const critLevel = new Utils.IntegerHolder(0); - partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel); - expect(critLevel.value).toBe(0); + const enemyMember = game.scene.getEnemyPokemon()!; - // Giving Leek to party member and testing if it applies - partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true); - partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel); + vi.spyOn(enemyMember, "getCritStage"); - expect(critLevel.value).toBe(2); + game.move.select(Moves.TACKLE); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(enemyMember.getCritStage).toHaveReturnedWith(2); }, 20000); - it("LEEK not held by FARFETCHD line", async () => { + it("should not raise CRIT stage when held by a Pokemon outside of FARFETCHD line", async () => { await game.startBattle([ Species.PIKACHU ]); - const partyMember = game.scene.getPlayerPokemon()!; + const enemyMember = game.scene.getEnemyPokemon()!; - // Making sure modifier is not applied without holding item - const critLevel = new Utils.IntegerHolder(0); - partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel); + vi.spyOn(enemyMember, "getCritStage"); - expect(critLevel.value).toBe(0); + game.move.select(Moves.TACKLE); - // Giving Leek to party member and testing if it applies - partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true); - partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel); + await game.phaseInterceptor.to(TurnEndPhase); - expect(critLevel.value).toBe(0); + expect(enemyMember.getCritStage).toHaveReturnedWith(0); }, 20000); }); diff --git a/src/test/items/light_ball.test.ts b/src/test/items/light_ball.test.ts index cf4f5c9e22f..673348e7b7a 100644 --- a/src/test/items/light_ball.test.ts +++ b/src/test/items/light_ball.test.ts @@ -1,4 +1,4 @@ -import { Stat } from "#app/data/pokemon-stat"; +import { Stat } from "#enums/stat"; import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; import { modifierTypes } from "#app/modifier/modifier-type"; import i18next from "#app/plugins/i18n"; @@ -37,29 +37,29 @@ describe("Items - Light Ball", () => { const partyMember = game.scene.getParty()[0]; - // Checking consoe log to make sure Light Ball is applied when getBattleStat (with the appropriate stat) is called - partyMember.getBattleStat(Stat.DEF); + // Checking console log to make sure Light Ball is applied when getEffectiveStat (with the appropriate stat) is called + partyMember.getEffectiveStat(Stat.DEF); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), ""); // Printing dummy console messages along the way so subsequent checks don't pass because of the first console.log(""); - partyMember.getBattleStat(Stat.SPDEF); + partyMember.getEffectiveStat(Stat.SPDEF); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), ""); console.log(""); - partyMember.getBattleStat(Stat.ATK); + partyMember.getEffectiveStat(Stat.ATK); expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), ""); console.log(""); - partyMember.getBattleStat(Stat.SPATK); + partyMember.getEffectiveStat(Stat.SPATK); expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), ""); console.log(""); - partyMember.getBattleStat(Stat.SPD); + partyMember.getEffectiveStat(Stat.SPD); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), ""); }); diff --git a/src/test/items/metal_powder.test.ts b/src/test/items/metal_powder.test.ts index a3a4936532f..0206fd1f471 100644 --- a/src/test/items/metal_powder.test.ts +++ b/src/test/items/metal_powder.test.ts @@ -1,4 +1,4 @@ -import { Stat } from "#app/data/pokemon-stat"; +import { Stat } from "#enums/stat"; import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; import { modifierTypes } from "#app/modifier/modifier-type"; import i18next from "#app/plugins/i18n"; @@ -37,29 +37,29 @@ describe("Items - Metal Powder", () => { const partyMember = game.scene.getParty()[0]; - // Checking consoe log to make sure Metal Powder is applied when getBattleStat (with the appropriate stat) is called - partyMember.getBattleStat(Stat.DEF); + // Checking console log to make sure Metal Powder is applied when getEffectiveStat (with the appropriate stat) is called + partyMember.getEffectiveStat(Stat.DEF); expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), ""); // Printing dummy console messages along the way so subsequent checks don't pass because of the first console.log(""); - partyMember.getBattleStat(Stat.SPDEF); + partyMember.getEffectiveStat(Stat.SPDEF); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), ""); console.log(""); - partyMember.getBattleStat(Stat.ATK); + partyMember.getEffectiveStat(Stat.ATK); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), ""); console.log(""); - partyMember.getBattleStat(Stat.SPATK); + partyMember.getEffectiveStat(Stat.SPATK); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), ""); console.log(""); - partyMember.getBattleStat(Stat.SPD); + partyMember.getEffectiveStat(Stat.SPD); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), ""); }); diff --git a/src/test/items/quick_powder.test.ts b/src/test/items/quick_powder.test.ts index 53521ba78f1..344b772feb4 100644 --- a/src/test/items/quick_powder.test.ts +++ b/src/test/items/quick_powder.test.ts @@ -1,4 +1,4 @@ -import { Stat } from "#app/data/pokemon-stat"; +import { Stat } from "#enums/stat"; import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; import { modifierTypes } from "#app/modifier/modifier-type"; import i18next from "#app/plugins/i18n"; @@ -37,29 +37,29 @@ describe("Items - Quick Powder", () => { const partyMember = game.scene.getParty()[0]; - // Checking consoe log to make sure Quick Powder is applied when getBattleStat (with the appropriate stat) is called - partyMember.getBattleStat(Stat.DEF); + // Checking console log to make sure Quick Powder is applied when getEffectiveStat (with the appropriate stat) is called + partyMember.getEffectiveStat(Stat.DEF); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.QUICK_POWDER.name"), ""); // Printing dummy console messages along the way so subsequent checks don't pass because of the first console.log(""); - partyMember.getBattleStat(Stat.SPDEF); + partyMember.getEffectiveStat(Stat.SPDEF); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.QUICK_POWDER.name"), ""); console.log(""); - partyMember.getBattleStat(Stat.ATK); + partyMember.getEffectiveStat(Stat.ATK); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.QUICK_POWDER.name"), ""); console.log(""); - partyMember.getBattleStat(Stat.SPATK); + partyMember.getEffectiveStat(Stat.SPATK); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.QUICK_POWDER.name"), ""); console.log(""); - partyMember.getBattleStat(Stat.SPD); + partyMember.getEffectiveStat(Stat.SPD); expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.QUICK_POWDER.name"), ""); }); diff --git a/src/test/items/scope_lens.test.ts b/src/test/items/scope_lens.test.ts index 85673218762..c8629093ab5 100644 --- a/src/test/items/scope_lens.test.ts +++ b/src/test/items/scope_lens.test.ts @@ -1,13 +1,10 @@ -import { BattlerIndex } from "#app/battle"; -import { CritBoosterModifier } from "#app/modifier/modifier"; -import { modifierTypes } from "#app/modifier/modifier-type"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -import * as Utils from "#app/utils"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phase from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { SPLASH_ONLY } from "../utils/testUtils"; describe("Items - Scope Lens", () => { let phaserGame: Phaser.Game; @@ -26,47 +23,29 @@ describe("Items - Scope Lens", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.enemySpecies(Species.MAGIKARP); - game.override.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); - game.override.disableCrits(); + game.override + .enemySpecies(Species.MAGIKARP) + .enemyMoveset(SPLASH_ONLY) + .moveset([ Moves.POUND ]) + .startingHeldItems([{ name: "SCOPE_LENS" }]) + .battleType("single") + .disableCrits(); - game.override.battleType("single"); }, 20000); - it("SCOPE_LENS activates in battle correctly", async () => { - game.override.startingHeldItems([{ name: "SCOPE_LENS" }]); - game.override.moveset([Moves.POUND]); - const consoleSpy = vi.spyOn(console, "log"); + it("should raise CRIT stage by 1", async () => { await game.startBattle([ Species.GASTLY ]); + const enemyPokemon = game.scene.getEnemyPokemon()!; + + vi.spyOn(enemyPokemon, "getCritStage"); + game.move.select(Moves.POUND); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.phaseInterceptor.to(TurnEndPhase); - await game.phaseInterceptor.to(MoveEffectPhase); - - expect(consoleSpy).toHaveBeenCalledWith("Applied", "Scope Lens", ""); - }, 20000); - - it("SCOPE_LENS held by random pokemon", async () => { - await game.startBattle([ - Species.GASTLY - ]); - - const partyMember = game.scene.getPlayerPokemon()!; - - // Making sure modifier is not applied without holding item - const critLevel = new Utils.IntegerHolder(0); - partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel); - - expect(critLevel.value).toBe(0); - - // Giving Scope Lens to party member and testing if it applies - partyMember.scene.addModifier(modifierTypes.SCOPE_LENS().newModifier(partyMember), true); - partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel); - - expect(critLevel.value).toBe(1); + expect(enemyPokemon.getCritStage).toHaveReturnedWith(1); }, 20000); }); diff --git a/src/test/items/temp_stat_stage_booster.test.ts b/src/test/items/temp_stat_stage_booster.test.ts new file mode 100644 index 00000000000..e5b95c6c3b6 --- /dev/null +++ b/src/test/items/temp_stat_stage_booster.test.ts @@ -0,0 +1,174 @@ +import { BATTLE_STATS, Stat } from "#enums/stat"; +import GameManager from "#test/utils/gameManager"; +import { Species } from "#enums/species"; +import Phase from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { Moves } from "#app/enums/moves"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { SPLASH_ONLY } from "../utils/testUtils"; +import { Abilities } from "#app/enums/abilities"; +import { TempStatStageBoosterModifier } from "#app/modifier/modifier"; +import { Mode } from "#app/ui/ui"; +import { Button } from "#app/enums/buttons"; +import { CommandPhase } from "#app/phases/command-phase"; +import { NewBattlePhase } from "#app/phases/new-battle-phase"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { TurnInitPhase } from "#app/phases/turn-init-phase"; +import { BattleEndPhase } from "#app/phases/battle-end-phase"; +import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; + + +describe("Items - Temporary Stat Stage Boosters", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phase.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override + .battleType("single") + .enemySpecies(Species.SHUCKLE) + .enemyMoveset(SPLASH_ONLY) + .enemyAbility(Abilities.BALL_FETCH) + .moveset([ Moves.TACKLE, Moves.SPLASH, Moves.HONE_CLAWS, Moves.BELLY_DRUM ]) + .startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ATK }]); + }); + + it("should provide a x1.3 stat stage multiplier", async() => { + await game.startBattle([ + Species.PIKACHU + ]); + + const partyMember = game.scene.getPlayerPokemon()!; + + vi.spyOn(partyMember, "getStatStageMultiplier"); + + game.move.select(Moves.TACKLE); + + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); + + expect(partyMember.getStatStageMultiplier).toHaveReturnedWith(1.3); + }, 20000); + + it("should increase existing ACC stat stage by 1 for X_ACCURACY only", async() => { + game.override + .startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }]) + .ability(Abilities.SIMPLE); + + await game.startBattle([ + Species.PIKACHU + ]); + + const partyMember = game.scene.getPlayerPokemon()!; + + vi.spyOn(partyMember, "getAccuracyMultiplier"); + + // Raise ACC by +2 stat stages + game.move.select(Moves.HONE_CLAWS); + + await game.phaseInterceptor.to(TurnEndPhase); + + game.move.select(Moves.TACKLE); + + await game.phaseInterceptor.to(TurnEndPhase); + + // ACC at +3 stat stages yields a x2 multiplier + expect(partyMember.getAccuracyMultiplier).toHaveReturnedWith(2); + }, 20000); + + + it("should increase existing stat stage multiplier by 3/10 for the rest of the boosters", async() => { + await game.startBattle([ + Species.PIKACHU + ]); + + const partyMember = game.scene.getPlayerPokemon()!; + + vi.spyOn(partyMember, "getStatStageMultiplier"); + + // Raise ATK by +1 stat stage + game.move.select(Moves.HONE_CLAWS); + + await game.phaseInterceptor.to(TurnEndPhase); + + game.move.select(Moves.TACKLE); + + await game.phaseInterceptor.to(TurnEndPhase); + + // ATK at +1 stat stage yields a x1.5 multiplier, add 0.3 from X_ATTACK + expect(partyMember.getStatStageMultiplier).toHaveReturnedWith(1.8); + }, 20000); + + it("should not increase past maximum stat stage multiplier", async() => { + game.override.startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }, { name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ATK }]); + + await game.startBattle([ + Species.PIKACHU + ]); + + const partyMember = game.scene.getPlayerPokemon()!; + + vi.spyOn(partyMember, "getStatStageMultiplier"); + vi.spyOn(partyMember, "getAccuracyMultiplier"); + + // Set all stat stages to 6 + vi.spyOn(partyMember.summonData, "statStages", "get").mockReturnValue(new Array(BATTLE_STATS.length).fill(6)); + + game.move.select(Moves.TACKLE); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(partyMember.getAccuracyMultiplier).toHaveReturnedWith(3); + expect(partyMember.getStatStageMultiplier).toHaveReturnedWith(4); + }, 20000); + + it("should renew how many battles are left of existing booster when picking up new booster of same type", async() => { + game.override + .startingLevel(200) + .itemRewards([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ATK }]); + + await game.startBattle([ + Species.PIKACHU + ]); + + game.move.select(Moves.SPLASH); + + await game.doKillOpponents(); + + await game.phaseInterceptor.to(BattleEndPhase); + + const modifier = game.scene.findModifier(m => m instanceof TempStatStageBoosterModifier) as TempStatStageBoosterModifier; + expect(modifier.getBattlesLeft()).toBe(4); + + // Forced X_ATTACK to spawn in the first slot with override + game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => { + const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler; + // Traverse to first modifier slot + handler.processInput(Button.LEFT); + handler.processInput(Button.UP); + handler.processInput(Button.ACTION); + }, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(NewBattlePhase), true); + + await game.phaseInterceptor.to(TurnInitPhase); + + // Making sure only one booster is in the modifier list even after picking up another + let count = 0; + for (const m of game.scene.modifiers) { + if (m instanceof TempStatStageBoosterModifier) { + count++; + expect((m as TempStatStageBoosterModifier).getBattlesLeft()).toBe(5); + } + } + expect(count).toBe(1); + }, 20000); +}); diff --git a/src/test/items/thick_club.test.ts b/src/test/items/thick_club.test.ts index 347921446e6..bcb6b371264 100644 --- a/src/test/items/thick_club.test.ts +++ b/src/test/items/thick_club.test.ts @@ -1,4 +1,4 @@ -import { Stat } from "#app/data/pokemon-stat"; +import { Stat } from "#enums/stat"; import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; import { modifierTypes } from "#app/modifier/modifier-type"; import i18next from "#app/plugins/i18n"; @@ -37,29 +37,29 @@ describe("Items - Thick Club", () => { const partyMember = game.scene.getParty()[0]; - // Checking consoe log to make sure Thick Club is applied when getBattleStat (with the appropriate stat) is called - partyMember.getBattleStat(Stat.DEF); + // Checking console log to make sure Thick Club is applied when getEffectiveStat (with the appropriate stat) is called + partyMember.getEffectiveStat(Stat.DEF); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.THICK_CLUB.name"), ""); // Printing dummy console messages along the way so subsequent checks don't pass because of the first console.log(""); - partyMember.getBattleStat(Stat.SPDEF); + partyMember.getEffectiveStat(Stat.SPDEF); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.THICK_CLUB.name"), ""); console.log(""); - partyMember.getBattleStat(Stat.ATK); + partyMember.getEffectiveStat(Stat.ATK); expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.THICK_CLUB.name"), ""); console.log(""); - partyMember.getBattleStat(Stat.SPATK); + partyMember.getEffectiveStat(Stat.SPATK); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.THICK_CLUB.name"), ""); console.log(""); - partyMember.getBattleStat(Stat.SPD); + partyMember.getEffectiveStat(Stat.SPD); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.THICK_CLUB.name"), ""); }); diff --git a/src/test/localization/battle-stat.test.ts b/src/test/localization/battle-stat.test.ts deleted file mode 100644 index b5ba698c4b6..00000000000 --- a/src/test/localization/battle-stat.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "#app/data/battle-stat"; -import deBattleStat from "#app/locales/de/battle.json"; -import dePokemonInfo from "#app/locales/de/pokemon-info.json"; -import enBattleStat from "#app/locales/en/battle.json"; -import enPokemonInfo from "#app/locales/en/pokemon-info.json"; -import esBattleStat from "#app/locales/es/battle.json"; -import esPokemonInfo from "#app/locales/es/pokemon-info.json"; -import frBattleStat from "#app/locales/fr/battle.json"; -import frPokemonInfo from "#app/locales/fr/pokemon-info.json"; -import itBattleStat from "#app/locales/it/battle.json"; -import itPokemonInfo from "#app/locales/it/pokemon-info.json"; -import koBattleStat from "#app/locales/ko/battle.json"; -import koPokemonInfo from "#app/locales/ko/pokemon-info.json"; -import ptBrBattleStat from "#app/locales/pt_BR/battle.json"; -import ptBrPokemonInfo from "#app/locales/pt_BR/pokemon-info.json"; -import zhCnBattleStat from "#app/locales/zh_CN/battle.json"; -import zhCnPokemonInfo from "#app/locales/zh_CN/pokemon-info.json"; -import zhTwBattleStat from "#app/locales/zh_TW/battle.json"; -import zhTwPokemonInfo from "#app/locales/zh_TW/pokemon-info.json"; -import i18next, { initI18n } from "#app/plugins/i18n"; -import { KoreanPostpositionProcessor } from "i18next-korean-postposition-processor"; -import { beforeAll, describe, expect, it } from "vitest"; - -interface BattleStatTestUnit { - stat: BattleStat, - key: string -} - -interface BattleStatLevelTestUnit { - levels: integer, - up: boolean, - key: string - changedStats: integer -} - -function testBattleStatName(stat: BattleStat, expectMessage: string) { - if (!expectMessage) { - return; - } // not translated yet! - const message = getBattleStatName(stat); - console.log(`message ${message}, expected ${expectMessage}`); - expect(message).toBe(expectMessage); -} - -function testBattleStatLevelChangeDescription(levels: integer, up: boolean, expectMessage: string, changedStats: integer) { - if (!expectMessage) { - return; - } // not translated yet! - const message = getBattleStatLevelChangeDescription("{{pokemonNameWithAffix}}", "{{stats}}", levels, up, changedStats); - console.log(`message ${message}, expected ${expectMessage}`); - expect(message).toBe(expectMessage); -} - -describe("Test for BattleStat Localization", () => { - const battleStatUnits: BattleStatTestUnit[] = []; - const battleStatLevelUnits: BattleStatLevelTestUnit[] = []; - - beforeAll(() => { - initI18n(); - - battleStatUnits.push({stat: BattleStat.ATK, key: "Stat.ATK"}); - battleStatUnits.push({stat: BattleStat.DEF, key: "Stat.DEF"}); - battleStatUnits.push({stat: BattleStat.SPATK, key: "Stat.SPATK"}); - battleStatUnits.push({stat: BattleStat.SPDEF, key: "Stat.SPDEF"}); - battleStatUnits.push({stat: BattleStat.SPD, key: "Stat.SPD"}); - battleStatUnits.push({stat: BattleStat.ACC, key: "Stat.ACC"}); - battleStatUnits.push({stat: BattleStat.EVA, key: "Stat.EVA"}); - - battleStatLevelUnits.push({levels: 1, up: true, key: "statRose_one", changedStats: 1}); - battleStatLevelUnits.push({levels: 2, up: true, key: "statSharplyRose_one", changedStats: 1}); - battleStatLevelUnits.push({levels: 3, up: true, key: "statRoseDrastically_one", changedStats: 1}); - battleStatLevelUnits.push({levels: 4, up: true, key: "statRoseDrastically_one", changedStats: 1}); - battleStatLevelUnits.push({levels: 5, up: true, key: "statRoseDrastically_one", changedStats: 1}); - battleStatLevelUnits.push({levels: 6, up: true, key: "statRoseDrastically_one", changedStats: 1}); - battleStatLevelUnits.push({levels: 7, up: true, key: "statWontGoAnyHigher_one", changedStats: 1}); - battleStatLevelUnits.push({levels: 1, up: false, key: "statFell_one", changedStats: 1}); - battleStatLevelUnits.push({levels: 2, up: false, key: "statHarshlyFell_one", changedStats: 1}); - battleStatLevelUnits.push({levels: 3, up: false, key: "statSeverelyFell_one", changedStats: 1}); - battleStatLevelUnits.push({levels: 4, up: false, key: "statSeverelyFell_one", changedStats: 1}); - battleStatLevelUnits.push({levels: 5, up: false, key: "statSeverelyFell_one", changedStats: 1}); - battleStatLevelUnits.push({levels: 6, up: false, key: "statSeverelyFell_one", changedStats: 1}); - battleStatLevelUnits.push({levels: 7, up: false, key: "statWontGoAnyLower_one", changedStats: 1}); - }); - - it("Test getBattleStatName() in English", async () => { - i18next.changeLanguage("en"); - battleStatUnits.forEach(unit => { - testBattleStatName(unit.stat, enPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]); - }); - }); - - it("Test getBattleStatLevelChangeDescription() in English", async () => { - i18next.changeLanguage("en"); - battleStatLevelUnits.forEach(unit => { - testBattleStatLevelChangeDescription(unit.levels, unit.up, enBattleStat[unit.key], unit.changedStats); - }); - }); - - it("Test getBattleStatName() in Español", async () => { - i18next.changeLanguage("es"); - battleStatUnits.forEach(unit => { - testBattleStatName(unit.stat, esPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]); - }); - }); - - it("Test getBattleStatLevelChangeDescription() in Español", async () => { - i18next.changeLanguage("es"); - battleStatLevelUnits.forEach(unit => { - testBattleStatLevelChangeDescription(unit.levels, unit.up, esBattleStat[unit.key], unit.changedStats); - }); - }); - - it("Test getBattleStatName() in Italiano", async () => { - i18next.changeLanguage("it"); - battleStatUnits.forEach(unit => { - testBattleStatName(unit.stat, itPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]); - }); - }); - - it("Test getBattleStatLevelChangeDescription() in Italiano", async () => { - i18next.changeLanguage("it"); - battleStatLevelUnits.forEach(unit => { - testBattleStatLevelChangeDescription(unit.levels, unit.up, itBattleStat[unit.key], unit.changedStats); - }); - }); - - it("Test getBattleStatName() in Français", async () => { - i18next.changeLanguage("fr"); - battleStatUnits.forEach(unit => { - testBattleStatName(unit.stat, frPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]); - }); - }); - - it("Test getBattleStatLevelChangeDescription() in Français", async () => { - i18next.changeLanguage("fr"); - battleStatLevelUnits.forEach(unit => { - testBattleStatLevelChangeDescription(unit.levels, unit.up, frBattleStat[unit.key], unit.changedStats); - }); - }); - - it("Test getBattleStatName() in Deutsch", async () => { - i18next.changeLanguage("de"); - battleStatUnits.forEach(unit => { - testBattleStatName(unit.stat, dePokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]); - }); - }); - - it("Test getBattleStatLevelChangeDescription() in Deutsch", async () => { - i18next.changeLanguage("de"); - battleStatLevelUnits.forEach(unit => { - testBattleStatLevelChangeDescription(unit.levels, unit.up, deBattleStat[unit.key], unit.changedStats); - }); - }); - - it("Test getBattleStatName() in Português (BR)", async () => { - i18next.changeLanguage("pt-BR"); - battleStatUnits.forEach(unit => { - testBattleStatName(unit.stat, ptBrPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]); - }); - }); - - it("Test getBattleStatLevelChangeDescription() in Português (BR)", async () => { - i18next.changeLanguage("pt-BR"); - battleStatLevelUnits.forEach(unit => { - testBattleStatLevelChangeDescription(unit.levels, unit.up, ptBrBattleStat[unit.key], unit.changedStats); - }); - }); - - it("Test getBattleStatName() in 简体中文", async () => { - i18next.changeLanguage("zh-CN"); - battleStatUnits.forEach(unit => { - testBattleStatName(unit.stat, zhCnPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]); - }); - }); - - it("Test getBattleStatLevelChangeDescription() in 简体中文", async () => { - i18next.changeLanguage("zh-CN"); - battleStatLevelUnits.forEach(unit => { - // In i18next, the pluralization rules are language-specific, and Chinese only supports the _other suffix. - unit.key = unit.key.replace("one", "other"); - testBattleStatLevelChangeDescription(unit.levels, unit.up, zhCnBattleStat[unit.key], unit.changedStats); - }); - }); - - it("Test getBattleStatName() in 繁體中文", async () => { - i18next.changeLanguage("zh-TW"); - battleStatUnits.forEach(unit => { - testBattleStatName(unit.stat, zhTwPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]); - }); - }); - - it("Test getBattleStatLevelChangeDescription() in 繁體中文", async () => { - i18next.changeLanguage("zh-TW"); - battleStatLevelUnits.forEach(unit => { - // In i18next, the pluralization rules are language-specific, and Chinese only supports the _other suffix. - unit.key = unit.key.replace("one", "other"); - testBattleStatLevelChangeDescription(unit.levels, unit.up, zhTwBattleStat[unit.key], unit.changedStats); - }); - }); - - it("Test getBattleStatName() in 한국어", async () => { - await i18next.changeLanguage("ko"); - battleStatUnits.forEach(unit => { - testBattleStatName(unit.stat, koPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]); - }); - }); - - it("Test getBattleStatLevelChangeDescription() in 한국어", async () => { - i18next.changeLanguage("ko", () => { - battleStatLevelUnits.forEach(unit => { - const processor = new KoreanPostpositionProcessor(); - const message = processor.process(koBattleStat[unit.key]); - testBattleStatLevelChangeDescription(unit.levels, unit.up, message, unit.changedStats); - }); - }); - }); -}); diff --git a/src/test/moves/alluring_voice.test.ts b/src/test/moves/alluring_voice.test.ts index e6ece39524a..9807d1bce85 100644 --- a/src/test/moves/alluring_voice.test.ts +++ b/src/test/moves/alluring_voice.test.ts @@ -40,7 +40,7 @@ describe("Moves - Alluring Voice", () => { }); - it("should confuse the opponent if their stats were raised", async () => { + it("should confuse the opponent if their stat stages were raised", async () => { await game.classicMode.startBattle(); const enemy = game.scene.getEnemyPokemon()!; diff --git a/src/test/moves/baton_pass.test.ts b/src/test/moves/baton_pass.test.ts index 602da9e37f8..0643b73e481 100644 --- a/src/test/moves/baton_pass.test.ts +++ b/src/test/moves/baton_pass.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { PostSummonPhase } from "#app/phases/post-summon-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import GameManager from "#app/test/utils/gameManager"; @@ -35,7 +35,7 @@ describe("Moves - Baton Pass", () => { .disableCrits(); }); - it("passes stat stage buffs when player uses it", async () => { + it("transfers all stat stages when player uses it", async() => { // arrange await game.startBattle([ Species.RAICHU, @@ -45,7 +45,10 @@ describe("Moves - Baton Pass", () => { // round 1 - buff game.move.select(Moves.NASTY_PLOT); await game.toNextTurn(); - expect(game.scene.getPlayerPokemon()!.summonData.battleStats[BattleStat.SPATK]).toEqual(2); + + let playerPokemon = game.scene.getPlayerPokemon()!; + + expect(playerPokemon.getStatStage(Stat.SPATK)).toEqual(2); // round 2 - baton pass game.move.select(Moves.BATON_PASS); @@ -53,9 +56,9 @@ describe("Moves - Baton Pass", () => { await game.phaseInterceptor.to(TurnEndPhase); // assert - const playerPkm = game.scene.getPlayerPokemon()!; - expect(playerPkm.species.speciesId).toEqual(Species.SHUCKLE); - expect(playerPkm.summonData.battleStats[BattleStat.SPATK]).toEqual(2); + playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.species.speciesId).toEqual(Species.SHUCKLE); + expect(playerPokemon.getStatStage(Stat.SPATK)).toEqual(2); }, 20000); it("passes stat stage buffs when AI uses it", async () => { @@ -80,7 +83,7 @@ describe("Moves - Baton Pass", () => { // assert // check buffs are still there - expect(game.scene.getEnemyPokemon()!.summonData.battleStats[BattleStat.SPATK]).toEqual(2); + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.SPATK)).toEqual(2); // confirm that a switch actually happened. can't use species because I // can't find a way to override trainer parties with more than 1 pokemon species expect(game.scene.getEnemyPokemon()!.hp).not.toEqual(100); diff --git a/src/test/moves/belly_drum.test.ts b/src/test/moves/belly_drum.test.ts index e4956c6e83a..7024deb3f18 100644 --- a/src/test/moves/belly_drum.test.ts +++ b/src/test/moves/belly_drum.test.ts @@ -1,8 +1,8 @@ -import { BattleStat } from "#app/data/battle-stat"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { toDmgValue } from "#app/utils"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; @@ -43,8 +43,8 @@ describe("Moves - BELLY DRUM", () => { // Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Belly_Drum_(move) - test("Belly Drum raises the user's Attack to its max, at the cost of 1/2 of its maximum HP", - async () => { + test("raises the user's ATK stat stage to its max, at the cost of 1/2 of its maximum HP", + async() => { await game.startBattle([Species.MAGIKARP]); const leadPokemon = game.scene.getPlayerPokemon()!; @@ -54,48 +54,48 @@ describe("Moves - BELLY DRUM", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost); - expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6); }, TIMEOUT ); - test("Belly Drum will still take effect if an uninvolved stat is at max", - async () => { + test("will still take effect if an uninvolved stat stage is at max", + async() => { await game.startBattle([Species.MAGIKARP]); const leadPokemon = game.scene.getPlayerPokemon()!; const hpLost = toDmgValue(leadPokemon.getMaxHp() / RATIO); - // Here - BattleStat.ATK -> -3 and BattleStat.SPATK -> 6 - leadPokemon.summonData.battleStats[BattleStat.ATK] = -3; - leadPokemon.summonData.battleStats[BattleStat.SPATK] = 6; + // Here - Stat.ATK -> -3 and Stat.SPATK -> 6 + leadPokemon.setStatStage(Stat.ATK, -3); + leadPokemon.setStatStage(Stat.SPATK, 6); game.move.select(Moves.BELLY_DRUM); await game.phaseInterceptor.to(TurnEndPhase); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost); - expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6); - expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(6); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6); + expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(6); }, TIMEOUT ); - test("Belly Drum fails if the pokemon's attack stat is at its maximum", - async () => { + test("fails if the pokemon's ATK stat stage is at its maximum", + async() => { await game.startBattle([Species.MAGIKARP]); const leadPokemon = game.scene.getPlayerPokemon()!; - leadPokemon.summonData.battleStats[BattleStat.ATK] = 6; + leadPokemon.setStatStage(Stat.ATK, 6); game.move.select(Moves.BELLY_DRUM); await game.phaseInterceptor.to(TurnEndPhase); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6); }, TIMEOUT ); - test("Belly Drum fails if the user's health is less than 1/2", - async () => { + test("fails if the user's health is less than 1/2", + async() => { await game.startBattle([Species.MAGIKARP]); const leadPokemon = game.scene.getPlayerPokemon()!; @@ -106,7 +106,7 @@ describe("Moves - BELLY DRUM", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(leadPokemon.hp).toBe(hpLost - PREDAMAGE); - expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0); }, TIMEOUT ); }); diff --git a/src/test/moves/burning_jealousy.test.ts b/src/test/moves/burning_jealousy.test.ts index 2281fe74acb..2cb6a0bc52a 100644 --- a/src/test/moves/burning_jealousy.test.ts +++ b/src/test/moves/burning_jealousy.test.ts @@ -41,7 +41,7 @@ describe("Moves - Burning Jealousy", () => { }); - it("should burn the opponent if their stats were raised", async () => { + it("should burn the opponent if their stat stages were raised", async () => { await game.classicMode.startBattle(); const enemy = game.scene.getEnemyPokemon()!; @@ -53,7 +53,7 @@ describe("Moves - Burning Jealousy", () => { expect(enemy.status?.effect).toBe(StatusEffect.BURN); }, TIMEOUT); - it("should still burn the opponent if their stats were both raised and lowered in the same turn", async () => { + it("should still burn the opponent if their stat stages were both raised and lowered in the same turn", async () => { game.override .starterSpecies(0) .battleType("double"); @@ -69,7 +69,7 @@ describe("Moves - Burning Jealousy", () => { expect(enemy.status?.effect).toBe(StatusEffect.BURN); }, TIMEOUT); - it("should ignore stats raised by imposter", async () => { + it("should ignore stat stages raised by IMPOSTER", async () => { game.override .enemySpecies(Species.DITTO) .enemyAbility(Abilities.IMPOSTER) @@ -88,7 +88,7 @@ describe("Moves - Burning Jealousy", () => { await game.classicMode.startBattle(); }, TIMEOUT); - it("should be boosted by Sheer Force even if opponent didn't raise stats", async () => { + it("should be boosted by Sheer Force even if opponent didn't raise stat stages", async () => { game.override .ability(Abilities.SHEER_FORCE) .enemyMoveset(SPLASH_ONLY); diff --git a/src/test/moves/clangorous_soul.test.ts b/src/test/moves/clangorous_soul.test.ts index 9ea6da91595..9bd3bc2379e 100644 --- a/src/test/moves/clangorous_soul.test.ts +++ b/src/test/moves/clangorous_soul.test.ts @@ -1,12 +1,11 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#test/utils/gameManager"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; -import { toDmgValue } from "#app/utils"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import GameManager from "#test/utils/gameManager"; +import { Stat } from "#enums/stat"; import { SPLASH_ONLY } from "#test/utils/testUtils"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; const TIMEOUT = 20 * 1000; /** HP Cost of Move */ @@ -14,7 +13,7 @@ const RATIO = 3; /** Amount of extra HP lost */ const PREDAMAGE = 15; -describe("Moves - CLANGOROUS_SOUL", () => { +describe("Moves - Clangorous Soul", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -40,91 +39,91 @@ describe("Moves - CLANGOROUS_SOUL", () => { //Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Clangorous_Soul_(move) - test("Clangorous Soul raises the user's Attack, Defense, Special Attack, Special Defense and Speed by one stage each, at the cost of 1/3 of its maximum HP", - async () => { + it("raises the user's ATK, DEF, SPATK, SPDEF, and SPD stat stages by 1 each at the cost of 1/3 of its maximum HP", + async() => { await game.startBattle([Species.MAGIKARP]); const leadPokemon = game.scene.getPlayerPokemon()!; - const hpLost = toDmgValue(leadPokemon.getMaxHp() / RATIO); + const hpLost = Math.floor(leadPokemon.getMaxHp() / RATIO); game.move.select(Moves.CLANGOROUS_SOUL); await game.phaseInterceptor.to(TurnEndPhase); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost); - expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(1); - expect(leadPokemon.summonData.battleStats[BattleStat.DEF]).toBe(1); - expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(1); - expect(leadPokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(1); - expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(1); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(1); + expect(leadPokemon.getStatStage(Stat.DEF)).toBe(1); + expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(1); + expect(leadPokemon.getStatStage(Stat.SPDEF)).toBe(1); + expect(leadPokemon.getStatStage(Stat.SPD)).toBe(1); }, TIMEOUT ); - test("Clangorous Soul will still take effect if one or more of the involved stats are not at max", - async () => { + it("will still take effect if one or more of the involved stat stages are not at max", + async() => { await game.startBattle([Species.MAGIKARP]); const leadPokemon = game.scene.getPlayerPokemon()!; - const hpLost = toDmgValue(leadPokemon.getMaxHp() / RATIO); + const hpLost = Math.floor(leadPokemon.getMaxHp() / RATIO); - //Here - BattleStat.SPD -> 0 and BattleStat.SPDEF -> 4 - leadPokemon.summonData.battleStats[BattleStat.ATK] = 6; - leadPokemon.summonData.battleStats[BattleStat.DEF] = 6; - leadPokemon.summonData.battleStats[BattleStat.SPATK] = 6; - leadPokemon.summonData.battleStats[BattleStat.SPDEF] = 4; + //Here - Stat.SPD -> 0 and Stat.SPDEF -> 4 + leadPokemon.setStatStage(Stat.ATK, 6); + leadPokemon.setStatStage(Stat.DEF, 6); + leadPokemon.setStatStage(Stat.SPATK, 6); + leadPokemon.setStatStage(Stat.SPDEF, 4); game.move.select(Moves.CLANGOROUS_SOUL); await game.phaseInterceptor.to(TurnEndPhase); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost); - expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6); - expect(leadPokemon.summonData.battleStats[BattleStat.DEF]).toBe(6); - expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(6); - expect(leadPokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(5); - expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(1); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6); + expect(leadPokemon.getStatStage(Stat.DEF)).toBe(6); + expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(6); + expect(leadPokemon.getStatStage(Stat.SPDEF)).toBe(5); + expect(leadPokemon.getStatStage(Stat.SPD)).toBe(1); }, TIMEOUT ); - test("Clangorous Soul fails if all stats involved are at max", - async () => { + it("fails if all stat stages involved are at max", + async() => { await game.startBattle([Species.MAGIKARP]); const leadPokemon = game.scene.getPlayerPokemon()!; - leadPokemon.summonData.battleStats[BattleStat.ATK] = 6; - leadPokemon.summonData.battleStats[BattleStat.DEF] = 6; - leadPokemon.summonData.battleStats[BattleStat.SPATK] = 6; - leadPokemon.summonData.battleStats[BattleStat.SPDEF] = 6; - leadPokemon.summonData.battleStats[BattleStat.SPD] = 6; + leadPokemon.setStatStage(Stat.ATK, 6); + leadPokemon.setStatStage(Stat.DEF, 6); + leadPokemon.setStatStage(Stat.SPATK, 6); + leadPokemon.setStatStage(Stat.SPDEF, 6); + leadPokemon.setStatStage(Stat.SPD, 6); game.move.select(Moves.CLANGOROUS_SOUL); await game.phaseInterceptor.to(TurnEndPhase); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6); - expect(leadPokemon.summonData.battleStats[BattleStat.DEF]).toBe(6); - expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(6); - expect(leadPokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(6); - expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(6); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6); + expect(leadPokemon.getStatStage(Stat.DEF)).toBe(6); + expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(6); + expect(leadPokemon.getStatStage(Stat.SPDEF)).toBe(6); + expect(leadPokemon.getStatStage(Stat.SPD)).toBe(6); }, TIMEOUT ); - test("Clangorous Soul fails if the user's health is less than 1/3", - async () => { + it("fails if the user's health is less than 1/3", + async() => { await game.startBattle([Species.MAGIKARP]); const leadPokemon = game.scene.getPlayerPokemon()!; - const hpLost = toDmgValue(leadPokemon.getMaxHp() / RATIO); + const hpLost = Math.floor(leadPokemon.getMaxHp() / RATIO); leadPokemon.hp = hpLost - PREDAMAGE; game.move.select(Moves.CLANGOROUS_SOUL); await game.phaseInterceptor.to(TurnEndPhase); expect(leadPokemon.hp).toBe(hpLost - PREDAMAGE); - expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0); - expect(leadPokemon.summonData.battleStats[BattleStat.DEF]).toBe(0); - expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(0); - expect(leadPokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(0); - expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(0); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(leadPokemon.getStatStage(Stat.DEF)).toBe(0); + expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(0); + expect(leadPokemon.getStatStage(Stat.SPDEF)).toBe(0); + expect(leadPokemon.getStatStage(Stat.SPD)).toBe(0); }, TIMEOUT ); }); diff --git a/src/test/moves/crafty_shield.test.ts b/src/test/moves/crafty_shield.test.ts index a341a50b0b9..e73a1fd256d 100644 --- a/src/test/moves/crafty_shield.test.ts +++ b/src/test/moves/crafty_shield.test.ts @@ -1,13 +1,13 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; -import { BerryPhase } from "#app/phases/berry-phase"; -import { CommandPhase } from "#app/phases/command-phase"; -import { Abilities } from "#enums/abilities"; -import { Moves } from "#enums/moves"; -import { Species } from "#enums/species"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; import GameManager from "../utils/gameManager"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Stat } from "#enums/stat"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { BerryPhase } from "#app/phases/berry-phase"; +import { CommandPhase } from "#app/phases/command-phase"; const TIMEOUT = 20 * 1000; @@ -55,7 +55,7 @@ describe("Moves - Crafty Shield", () => { await game.phaseInterceptor.to(BerryPhase, false); - leadPokemon.forEach(p => expect(p.summonData.battleStats[BattleStat.ATK]).toBe(0)); + leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0)); }, TIMEOUT ); @@ -117,8 +117,8 @@ describe("Moves - Crafty Shield", () => { await game.phaseInterceptor.to(BerryPhase, false); - expect(leadPokemon[0].summonData.battleStats[BattleStat.ATK]).toBe(0); - expect(leadPokemon[1].summonData.battleStats[BattleStat.ATK]).toBe(2); + expect(leadPokemon[0].getStatStage(Stat.ATK)).toBe(0); + expect(leadPokemon[1].getStatStage(Stat.ATK)).toBe(2); } ); }); diff --git a/src/test/moves/double_team.test.ts b/src/test/moves/double_team.test.ts index c45c8bd8516..fa224c8df9e 100644 --- a/src/test/moves/double_team.test.ts +++ b/src/test/moves/double_team.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { Abilities } from "#app/enums/abilities"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Moves } from "#enums/moves"; @@ -32,20 +32,20 @@ describe("Moves - Double Team", () => { game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); }); - it("increases the user's evasion by one stage.", async () => { + it("raises the user's EVA stat stage by 1", async () => { await game.startBattle([Species.MAGIKARP]); const ally = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; vi.spyOn(enemy, "getAccuracyMultiplier"); - expect(ally.summonData.battleStats[BattleStat.EVA]).toBe(0); + expect(ally.getStatStage(Stat.EVA)).toBe(0); game.move.select(Moves.DOUBLE_TEAM); await game.phaseInterceptor.to(TurnEndPhase); await game.toNextTurn(); - expect(ally.summonData.battleStats[BattleStat.EVA]).toBe(1); + expect(ally.getStatStage(Stat.EVA)).toBe(1); expect(enemy.getAccuracyMultiplier).toHaveReturnedWith(.75); }); }); diff --git a/src/test/moves/dragon_rage.test.ts b/src/test/moves/dragon_rage.test.ts index 223635575ab..5da6e082ce5 100644 --- a/src/test/moves/dragon_rage.test.ts +++ b/src/test/moves/dragon_rage.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { Type } from "#app/data/type"; import { Species } from "#app/enums/species"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; @@ -63,9 +63,8 @@ describe("Moves - Dragon Rage", () => { game.move.select(Moves.DRAGON_RAGE); await game.phaseInterceptor.to(TurnEndPhase); - const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp; - expect(damageDealt).toBe(dragonRageDamage); + expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage); }); it("ignores resistances", async () => { @@ -74,20 +73,18 @@ describe("Moves - Dragon Rage", () => { game.move.select(Moves.DRAGON_RAGE); await game.phaseInterceptor.to(TurnEndPhase); - const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp; - expect(damageDealt).toBe(dragonRageDamage); + expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage); }); - it("ignores stat changes", async () => { + it("ignores SPATK stat stages", async () => { game.override.disableCrits(); - partyPokemon.summonData.battleStats[BattleStat.SPATK] = 2; + partyPokemon.setStatStage(Stat.SPATK, 2); game.move.select(Moves.DRAGON_RAGE); await game.phaseInterceptor.to(TurnEndPhase); - const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp; - expect(damageDealt).toBe(dragonRageDamage); + expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage); }); it("ignores stab", async () => { @@ -96,9 +93,8 @@ describe("Moves - Dragon Rage", () => { game.move.select(Moves.DRAGON_RAGE); await game.phaseInterceptor.to(TurnEndPhase); - const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp; - expect(damageDealt).toBe(dragonRageDamage); + expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage); }); it("ignores criticals", async () => { @@ -106,20 +102,18 @@ describe("Moves - Dragon Rage", () => { game.move.select(Moves.DRAGON_RAGE); await game.phaseInterceptor.to(TurnEndPhase); - const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp; - expect(damageDealt).toBe(dragonRageDamage); + expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage); }); - it("ignores damage modification from abilities such as ice scales", async () => { + it("ignores damage modification from abilities, for example ICE_SCALES", async () => { game.override.disableCrits(); game.override.enemyAbility(Abilities.ICE_SCALES); game.move.select(Moves.DRAGON_RAGE); await game.phaseInterceptor.to(TurnEndPhase); - const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp; - expect(damageDealt).toBe(dragonRageDamage); + expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage); }); it("ignores multi hit", async () => { @@ -128,8 +122,7 @@ describe("Moves - Dragon Rage", () => { game.move.select(Moves.DRAGON_RAGE); await game.phaseInterceptor.to(TurnEndPhase); - const damageDealt = enemyPokemon.getMaxHp() - enemyPokemon.hp; - expect(damageDealt).toBe(dragonRageDamage); + expect(enemyPokemon.getInverseHp()).toBe(dragonRageDamage); }); }); diff --git a/src/test/moves/fillet_away.test.ts b/src/test/moves/fillet_away.test.ts index b2ff9e25dba..a639a86c5c1 100644 --- a/src/test/moves/fillet_away.test.ts +++ b/src/test/moves/fillet_away.test.ts @@ -1,8 +1,8 @@ -import { BattleStat } from "#app/data/battle-stat"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { toDmgValue } from "#app/utils"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; import GameManager from "#test/utils/gameManager"; import { SPLASH_ONLY } from "#test/utils/testUtils"; import Phaser from "phaser"; @@ -40,8 +40,8 @@ describe("Moves - FILLET AWAY", () => { //Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/fillet_away_(move) - test("Fillet Away raises the user's Attack, Special Attack, and Speed by two stages each, at the cost of 1/2 of its maximum HP", - async () => { + test("raises the user's ATK, SPATK, and SPD stat stages by 2 each, at the cost of 1/2 of its maximum HP", + async() => { await game.startBattle([Species.MAGIKARP]); const leadPokemon = game.scene.getPlayerPokemon()!; @@ -51,55 +51,55 @@ describe("Moves - FILLET AWAY", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost); - expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(2); - expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(2); - expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(2); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2); + expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(2); + expect(leadPokemon.getStatStage(Stat.SPD)).toBe(2); }, TIMEOUT ); - test("Fillet Away will still take effect if one or more of the involved stats are not at max", - async () => { + test("still takes effect if one or more of the involved stat stages are not at max", + async() => { await game.startBattle([Species.MAGIKARP]); const leadPokemon = game.scene.getPlayerPokemon()!; const hpLost = toDmgValue(leadPokemon.getMaxHp() / RATIO); - //Here - BattleStat.SPD -> 0 and BattleStat.SPATK -> 3 - leadPokemon.summonData.battleStats[BattleStat.ATK] = 6; - leadPokemon.summonData.battleStats[BattleStat.SPATK] = 3; + //Here - Stat.SPD -> 0 and Stat.SPATK -> 3 + leadPokemon.setStatStage(Stat.ATK, 6); + leadPokemon.setStatStage(Stat.SPATK, 3); game.move.select(Moves.FILLET_AWAY); await game.phaseInterceptor.to(TurnEndPhase); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost); - expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6); - expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(5); - expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(2); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6); + expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(5); + expect(leadPokemon.getStatStage(Stat.SPD)).toBe(2); }, TIMEOUT ); - test("Fillet Away fails if all stats involved are at max", - async () => { + test("fails if all stat stages involved are at max", + async() => { await game.startBattle([Species.MAGIKARP]); const leadPokemon = game.scene.getPlayerPokemon()!; - leadPokemon.summonData.battleStats[BattleStat.ATK] = 6; - leadPokemon.summonData.battleStats[BattleStat.SPATK] = 6; - leadPokemon.summonData.battleStats[BattleStat.SPD] = 6; + leadPokemon.setStatStage(Stat.ATK, 6); + leadPokemon.setStatStage(Stat.SPATK, 6); + leadPokemon.setStatStage(Stat.SPD, 6); game.move.select(Moves.FILLET_AWAY); await game.phaseInterceptor.to(TurnEndPhase); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6); - expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(6); - expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(6); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(6); + expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(6); + expect(leadPokemon.getStatStage(Stat.SPD)).toBe(6); }, TIMEOUT ); - test("Fillet Away fails if the user's health is less than 1/2", - async () => { + test("fails if the user's health is less than 1/2", + async() => { await game.startBattle([Species.MAGIKARP]); const leadPokemon = game.scene.getPlayerPokemon()!; @@ -110,9 +110,9 @@ describe("Moves - FILLET AWAY", () => { await game.phaseInterceptor.to(TurnEndPhase); expect(leadPokemon.hp).toBe(hpLost - PREDAMAGE); - expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0); - expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(0); - expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(0); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(leadPokemon.getStatStage(Stat.SPATK)).toBe(0); + expect(leadPokemon.getStatStage(Stat.SPD)).toBe(0); }, TIMEOUT ); }); diff --git a/src/test/moves/fissure.test.ts b/src/test/moves/fissure.test.ts index 51122b269b8..34612d1fb18 100644 --- a/src/test/moves/fissure.test.ts +++ b/src/test/moves/fissure.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { Species } from "#app/enums/species"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import { DamagePhase } from "#app/phases/damage-phase"; @@ -52,7 +52,7 @@ describe("Moves - Fissure", () => { game.scene.clearEnemyHeldItemModifiers(); }); - it("ignores damage modification from abilities such as fur coat", async () => { + it("ignores damage modification from abilities, for example FUR_COAT", async () => { game.override.ability(Abilities.NO_GUARD); game.override.enemyAbility(Abilities.FUR_COAT); @@ -62,10 +62,10 @@ describe("Moves - Fissure", () => { expect(enemyPokemon.isFainted()).toBe(true); }); - it("ignores accuracy stat", async () => { + it("ignores user's ACC stat stage", async () => { vi.spyOn(partyPokemon, "getAccuracyMultiplier"); - enemyPokemon.summonData.battleStats[BattleStat.ACC] = -6; + partyPokemon.setStatStage(Stat.ACC, -6); game.move.select(Moves.FISSURE); @@ -75,10 +75,10 @@ describe("Moves - Fissure", () => { expect(partyPokemon.getAccuracyMultiplier).toHaveReturnedWith(1); }); - it("ignores evasion stat", async () => { + it("ignores target's EVA stat stage", async () => { vi.spyOn(partyPokemon, "getAccuracyMultiplier"); - enemyPokemon.summonData.battleStats[BattleStat.EVA] = 6; + enemyPokemon.setStatStage(Stat.EVA, 6); game.move.select(Moves.FISSURE); diff --git a/src/test/moves/flower_shield.test.ts b/src/test/moves/flower_shield.test.ts index b3e50219aec..ffe8ae995d3 100644 --- a/src/test/moves/flower_shield.test.ts +++ b/src/test/moves/flower_shield.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { SemiInvulnerableTag } from "#app/data/battler-tags"; import { Type } from "#app/data/type"; import { Biome } from "#app/enums/biome"; @@ -34,24 +34,24 @@ describe("Moves - Flower Shield", () => { game.override.enemyMoveset(SPLASH_ONLY); }); - it("increases defense of all Grass-type Pokemon on the field by one stage - single battle", async () => { + it("raises DEF stat stage by 1 for all Grass-type Pokemon on the field by one stage - single battle", async () => { game.override.enemySpecies(Species.CHERRIM); await game.startBattle([Species.MAGIKARP]); const cherrim = game.scene.getEnemyPokemon()!; const magikarp = game.scene.getPlayerPokemon()!; - expect(magikarp.summonData.battleStats[BattleStat.DEF]).toBe(0); - expect(cherrim.summonData.battleStats[BattleStat.DEF]).toBe(0); + expect(magikarp.getStatStage(Stat.DEF)).toBe(0); + expect(cherrim.getStatStage(Stat.DEF)).toBe(0); game.move.select(Moves.FLOWER_SHIELD); await game.phaseInterceptor.to(TurnEndPhase); - expect(magikarp.summonData.battleStats[BattleStat.DEF]).toBe(0); - expect(cherrim.summonData.battleStats[BattleStat.DEF]).toBe(1); + expect(magikarp.getStatStage(Stat.DEF)).toBe(0); + expect(cherrim.getStatStage(Stat.DEF)).toBe(1); }); - it("increases defense of all Grass-type Pokemon on the field by one stage - double battle", async () => { + it("raises DEF stat stage by 1 for all Grass-type Pokemon on the field by one stage - double battle", async () => { game.override.enemySpecies(Species.MAGIKARP).startingBiome(Biome.GRASS).battleType("double"); await game.startBattle([Species.CHERRIM, Species.MAGIKARP]); @@ -60,21 +60,21 @@ describe("Moves - Flower Shield", () => { const grassPokemons = field.filter(p => p.getTypes().includes(Type.GRASS)); const nonGrassPokemons = field.filter(pokemon => !grassPokemons.includes(pokemon)); - grassPokemons.forEach(p => expect(p.summonData.battleStats[BattleStat.DEF]).toBe(0)); - nonGrassPokemons.forEach(p => expect(p.summonData.battleStats[BattleStat.DEF]).toBe(0)); + grassPokemons.forEach(p => expect(p.getStatStage(Stat.DEF)).toBe(0)); + nonGrassPokemons.forEach(p => expect(p.getStatStage(Stat.DEF)).toBe(0)); game.move.select(Moves.FLOWER_SHIELD); game.move.select(Moves.SPLASH, 1); await game.phaseInterceptor.to(TurnEndPhase); - grassPokemons.forEach(p => expect(p.summonData.battleStats[BattleStat.DEF]).toBe(1)); - nonGrassPokemons.forEach(p => expect(p.summonData.battleStats[BattleStat.DEF]).toBe(0)); + grassPokemons.forEach(p => expect(p.getStatStage(Stat.DEF)).toBe(1)); + nonGrassPokemons.forEach(p => expect(p.getStatStage(Stat.DEF)).toBe(0)); }); /** * See semi-vulnerable state tags. {@linkcode SemiInvulnerableTag} */ - it("does not increase defense of a pokemon in semi-vulnerable state", async () => { + it("does not raise DEF stat stage for a Pokemon in semi-vulnerable state", async () => { game.override.enemySpecies(Species.PARAS); game.override.enemyMoveset([Moves.DIG, Moves.DIG, Moves.DIG, Moves.DIG]); game.override.enemyLevel(50); @@ -83,32 +83,32 @@ describe("Moves - Flower Shield", () => { const paras = game.scene.getEnemyPokemon()!; const cherrim = game.scene.getPlayerPokemon()!; - expect(paras.summonData.battleStats[BattleStat.DEF]).toBe(0); - expect(cherrim.summonData.battleStats[BattleStat.DEF]).toBe(0); + expect(paras.getStatStage(Stat.DEF)).toBe(0); + expect(cherrim.getStatStage(Stat.DEF)).toBe(0); expect(paras.getTag(SemiInvulnerableTag)).toBeUndefined; game.move.select(Moves.FLOWER_SHIELD); await game.phaseInterceptor.to(TurnEndPhase); expect(paras.getTag(SemiInvulnerableTag)).toBeDefined(); - expect(paras.summonData.battleStats[BattleStat.DEF]).toBe(0); - expect(cherrim.summonData.battleStats[BattleStat.DEF]).toBe(1); + expect(paras.getStatStage(Stat.DEF)).toBe(0); + expect(cherrim.getStatStage(Stat.DEF)).toBe(1); }); - it("does nothing if there are no Grass-type pokemon on the field", async () => { + it("does nothing if there are no Grass-type Pokemon on the field", async () => { game.override.enemySpecies(Species.MAGIKARP); await game.startBattle([Species.MAGIKARP]); const enemy = game.scene.getEnemyPokemon()!; const ally = game.scene.getPlayerPokemon()!; - expect(enemy.summonData.battleStats[BattleStat.DEF]).toBe(0); - expect(ally.summonData.battleStats[BattleStat.DEF]).toBe(0); + expect(enemy.getStatStage(Stat.DEF)).toBe(0); + expect(ally.getStatStage(Stat.DEF)).toBe(0); game.move.select(Moves.FLOWER_SHIELD); await game.phaseInterceptor.to(TurnEndPhase); - expect(enemy.summonData.battleStats[BattleStat.DEF]).toBe(0); - expect(ally.summonData.battleStats[BattleStat.DEF]).toBe(0); + expect(enemy.getStatStage(Stat.DEF)).toBe(0); + expect(ally.getStatStage(Stat.DEF)).toBe(0); }); }); diff --git a/src/test/moves/follow_me.test.ts b/src/test/moves/follow_me.test.ts index d7ef199df3e..64fc9c16256 100644 --- a/src/test/moves/follow_me.test.ts +++ b/src/test/moves/follow_me.test.ts @@ -1,5 +1,5 @@ +import { Stat } from "#enums/stat"; import { BattlerIndex } from "#app/battle"; -import { Stat } from "#app/data/pokemon-stat"; import { Abilities } from "#app/enums/abilities"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Moves } from "#enums/moves"; @@ -66,7 +66,7 @@ describe("Moves - Follow Me", () => { game.move.select(Moves.FOLLOW_ME, 1); await game.phaseInterceptor.to(TurnEndPhase, false); - playerPokemon.sort((a, b) => a.getBattleStat(Stat.SPD) - b.getBattleStat(Stat.SPD)); + playerPokemon.sort((a, b) => a.getEffectiveStat(Stat.SPD) - b.getEffectiveStat(Stat.SPD)); expect(playerPokemon[1].hp).toBeLessThan(playerStartingHp[1]); expect(playerPokemon[0].hp).toBe(playerStartingHp[0]); diff --git a/src/test/moves/freezy_frost.test.ts b/src/test/moves/freezy_frost.test.ts index 00d7104d373..ae42d5b6dc6 100644 --- a/src/test/moves/freezy_frost.test.ts +++ b/src/test/moves/freezy_frost.test.ts @@ -1,82 +1,61 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { allMoves } from "#app/data/move"; -import { MoveEndPhase } from "#app/phases/move-end-phase"; -import { TurnInitPhase } from "#app/phases/turn-init-phase"; +import { Stat } from "#enums/stat"; +import GameManager from "#test/utils/gameManager"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import GameManager from "#test/utils/gameManager"; -import { SPLASH_ONLY } from "#test/utils/testUtils"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { SPLASH_ONLY } from "#test/utils/testUtils"; +import { allMoves } from "#app/data/move"; +import { TurnInitPhase } from "#app/phases/turn-init-phase"; describe("Moves - Freezy Frost", () => { - describe("integration tests", () => { - let phaserGame: Phaser.Game; - let game: GameManager; + let phaserGame: Phaser.Game; + let game: GameManager; - beforeAll(() => { - phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); - }); + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); - beforeEach(() => { - game = new GameManager(phaserGame); + beforeEach(() => { + game = new GameManager(phaserGame); - game.override.battleType("single"); + game.override.battleType("single"); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyLevel(100); - game.override.enemyMoveset(SPLASH_ONLY); - game.override.enemyAbility(Abilities.NONE); + game.override.enemySpecies(Species.RATTATA); + game.override.enemyLevel(100); + game.override.enemyMoveset(SPLASH_ONLY); + game.override.enemyAbility(Abilities.NONE); - game.override.startingLevel(100); - game.override.moveset([Moves.FREEZY_FROST, Moves.SWORDS_DANCE, Moves.CHARM, Moves.SPLASH]); - vi.spyOn(allMoves[Moves.FREEZY_FROST], "accuracy", "get").mockReturnValue(100); - game.override.ability(Abilities.NONE); - }); + game.override.startingLevel(100); + game.override.moveset([Moves.FREEZY_FROST, Moves.SWORDS_DANCE, Moves.CHARM, Moves.SPLASH]); + vi.spyOn(allMoves[Moves.FREEZY_FROST], "accuracy", "get").mockReturnValue(100); + game.override.ability(Abilities.NONE); + }); - it("Uses Swords Dance to raise own ATK by 2, Charm to lower enemy ATK by 2, player uses Freezy Frost to clear all stat changes", { timeout: 10000 }, async () => { - await game.startBattle([Species.RATTATA]); - const user = game.scene.getPlayerPokemon()!; - const enemy = game.scene.getEnemyPokemon()!; - expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0); - expect(enemy.summonData.battleStats[BattleStat.ATK]).toBe(0); + it("should clear all stat stage changes", { timeout: 10000 }, async () => { + await game.startBattle([Species.RATTATA]); + const user = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; - game.move.select(Moves.SWORDS_DANCE); - await game.phaseInterceptor.to(TurnInitPhase); + expect(user.getStatStage(Stat.ATK)).toBe(0); + expect(enemy.getStatStage(Stat.ATK)).toBe(0); - game.move.select(Moves.CHARM); - await game.phaseInterceptor.to(TurnInitPhase); - const userAtkBefore = user.summonData.battleStats[BattleStat.ATK]; - const enemyAtkBefore = enemy.summonData.battleStats[BattleStat.ATK]; - expect(userAtkBefore).toBe(2); - expect(enemyAtkBefore).toBe(-2); + game.move.select(Moves.SWORDS_DANCE); + await game.phaseInterceptor.to(TurnInitPhase); - game.move.select(Moves.FREEZY_FROST); - await game.phaseInterceptor.to(TurnInitPhase); - expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0); - expect(enemy.summonData.battleStats[BattleStat.ATK]).toBe(0); - }); + game.move.select(Moves.CHARM); + await game.phaseInterceptor.to(TurnInitPhase); + expect(user.getStatStage(Stat.ATK)).toBe(2); + expect(enemy.getStatStage(Stat.ATK)).toBe(-2); - it("Uses Swords Dance to raise own ATK by 2, Charm to lower enemy ATK by 2, enemy uses Freezy Frost to clear all stat changes", { timeout: 10000 }, async () => { - game.override.enemyMoveset([Moves.FREEZY_FROST, Moves.FREEZY_FROST, Moves.FREEZY_FROST, Moves.FREEZY_FROST]); - await game.startBattle([Species.SHUCKLE]); // Shuckle for slower Swords Dance on first turn so Freezy Frost doesn't affect it. - const user = game.scene.getPlayerPokemon()!; - expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0); - - game.move.select(Moves.SWORDS_DANCE); - await game.phaseInterceptor.to(TurnInitPhase); - - const userAtkBefore = user.summonData.battleStats[BattleStat.ATK]; - expect(userAtkBefore).toBe(2); - - game.move.select(Moves.SPLASH); - await game.phaseInterceptor.to(MoveEndPhase); - expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0); - }); + game.move.select(Moves.FREEZY_FROST); + await game.phaseInterceptor.to(TurnInitPhase); + expect(user.getStatStage(Stat.ATK)).toBe(0); + expect(enemy.getStatStage(Stat.ATK)).toBe(0); }); }); diff --git a/src/test/moves/fusion_flare_bolt.test.ts b/src/test/moves/fusion_flare_bolt.test.ts index ebef5148778..a8372fcaaab 100644 --- a/src/test/moves/fusion_flare_bolt.test.ts +++ b/src/test/moves/fusion_flare_bolt.test.ts @@ -1,6 +1,6 @@ +import { Stat } from "#enums/stat"; import { BattlerIndex } from "#app/battle"; import { allMoves } from "#app/data/move"; -import { Stat } from "#app/data/pokemon-stat"; import { DamagePhase } from "#app/phases/damage-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; diff --git a/src/test/moves/growth.test.ts b/src/test/moves/growth.test.ts index dfbf5406351..defe5e26f41 100644 --- a/src/test/moves/growth.test.ts +++ b/src/test/moves/growth.test.ts @@ -1,14 +1,13 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { Stat } from "#app/data/pokemon-stat"; -import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; -import { TurnInitPhase } from "#app/phases/turn-init-phase"; +import { Stat } from "#enums/stat"; +import GameManager from "#test/utils/gameManager"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - +import { SPLASH_ONLY } from "../utils/testUtils"; +import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; +import { TurnInitPhase } from "#app/phases/turn-init-phase"; describe("Moves - Growth", () => { let phaserGame: Phaser.Game; @@ -26,31 +25,25 @@ describe("Moves - Growth", () => { beforeEach(() => { game = new GameManager(phaserGame); - const moveToUse = Moves.GROWTH; game.override.battleType("single"); - game.override.enemySpecies(Species.RATTATA); game.override.enemyAbility(Abilities.MOXIE); game.override.ability(Abilities.INSOMNIA); - game.override.startingLevel(2000); - game.override.moveset([moveToUse]); - game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + game.override.moveset([ Moves.GROWTH ]); + game.override.enemyMoveset(SPLASH_ONLY); }); - it("GROWTH", async () => { - const moveToUse = Moves.GROWTH; + it("should raise SPATK stat stage by 1", async() => { await game.startBattle([ - Species.MIGHTYENA, - Species.MIGHTYENA, + Species.MIGHTYENA ]); - let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[Stat.SPATK]).toBe(0); - const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.SPATK]).toBe(0); + const playerPokemon = game.scene.getPlayerPokemon()!; - game.move.select(moveToUse); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(0); + + game.move.select(Moves.GROWTH); await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnInitPhase); - battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.SPATK]).toBe(1); + + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); }, 20000); }); diff --git a/src/test/moves/guard_split.test.ts b/src/test/moves/guard_split.test.ts new file mode 100644 index 00000000000..f95d09f726c --- /dev/null +++ b/src/test/moves/guard_split.test.ts @@ -0,0 +1,82 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import { Species } from "#enums/species"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { Moves } from "#enums/moves"; +import { Stat } from "#enums/stat"; +import { Abilities } from "#enums/abilities"; +import { SPLASH_ONLY } from "../utils/testUtils"; + +describe("Moves - Guard Split", () => { + 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 + .battleType("single") + .enemyAbility(Abilities.NONE) + .enemySpecies(Species.MEW) + .enemyLevel(200) + .moveset([ Moves.GUARD_SPLIT ]) + .ability(Abilities.NONE); + }); + + it("should average the user's DEF and SPDEF stats with those of the target", async () => { + game.override.enemyMoveset(SPLASH_ONLY); + await game.startBattle([ + Species.INDEEDEE + ]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + const avgDef = Math.floor((player.getStat(Stat.DEF, false) + enemy.getStat(Stat.DEF, false)) / 2); + const avgSpDef = Math.floor((player.getStat(Stat.SPDEF, false) + enemy.getStat(Stat.SPDEF, false)) / 2); + + game.move.select(Moves.GUARD_SPLIT); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStat(Stat.DEF, false)).toBe(avgDef); + expect(enemy.getStat(Stat.DEF, false)).toBe(avgDef); + + expect(player.getStat(Stat.SPDEF, false)).toBe(avgSpDef); + expect(enemy.getStat(Stat.SPDEF, false)).toBe(avgSpDef); + }, 20000); + + it("should be idempotent", async () => { + game.override.enemyMoveset(new Array(4).fill(Moves.GUARD_SPLIT)); + await game.startBattle([ + Species.INDEEDEE + ]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + const avgDef = Math.floor((player.getStat(Stat.DEF, false) + enemy.getStat(Stat.DEF, false)) / 2); + const avgSpDef = Math.floor((player.getStat(Stat.SPDEF, false) + enemy.getStat(Stat.SPDEF, false)) / 2); + + game.move.select(Moves.GUARD_SPLIT); + await game.phaseInterceptor.to(TurnEndPhase); + + game.move.select(Moves.GUARD_SPLIT); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStat(Stat.DEF, false)).toBe(avgDef); + expect(enemy.getStat(Stat.DEF, false)).toBe(avgDef); + + expect(player.getStat(Stat.SPDEF, false)).toBe(avgSpDef); + expect(enemy.getStat(Stat.SPDEF, false)).toBe(avgSpDef); + }, 20000); +}); diff --git a/src/test/moves/guard_swap.test.ts b/src/test/moves/guard_swap.test.ts new file mode 100644 index 00000000000..407d475de09 --- /dev/null +++ b/src/test/moves/guard_swap.test.ts @@ -0,0 +1,63 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import { Species } from "#enums/species"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { Moves } from "#enums/moves"; +import { Stat } from "#enums/stat"; +import { Abilities } from "#enums/abilities"; +import { MoveEndPhase } from "#app/phases/move-end-phase"; + +describe("Moves - Guard Swap", () => { + 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 + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(new Array(4).fill(Moves.SHELL_SMASH)) + .enemySpecies(Species.MEW) + .enemyLevel(200) + .moveset([ Moves.GUARD_SWAP ]) + .ability(Abilities.NONE); + }); + + it("should swap the user's DEF AND SPDEF stat stages with the target's", async () => { + await game.startBattle([ + Species.INDEEDEE + ]); + + // Should start with no stat stages + const player = game.scene.getPlayerPokemon()!; + // After Shell Smash, should have +2 in ATK and SPATK, -1 in DEF and SPDEF + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.GUARD_SWAP); + + await game.phaseInterceptor.to(MoveEndPhase); + + expect(player.getStatStage(Stat.DEF)).toBe(0); + expect(player.getStatStage(Stat.SPDEF)).toBe(0); + expect(enemy.getStatStage(Stat.DEF)).toBe(-1); + expect(enemy.getStatStage(Stat.SPDEF)).toBe(-1); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.DEF)).toBe(-1); + expect(player.getStatStage(Stat.SPDEF)).toBe(-1); + expect(enemy.getStatStage(Stat.DEF)).toBe(0); + expect(enemy.getStatStage(Stat.SPDEF)).toBe(0); + }, 20000); +}); diff --git a/src/test/moves/haze.test.ts b/src/test/moves/haze.test.ts index 8a32a40cb32..42081ce74e8 100644 --- a/src/test/moves/haze.test.ts +++ b/src/test/moves/haze.test.ts @@ -1,13 +1,12 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { MoveEndPhase } from "#app/phases/move-end-phase"; -import { TurnInitPhase } from "#app/phases/turn-init-phase"; +import { Stat } from "#enums/stat"; +import GameManager from "#test/utils/gameManager"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import GameManager from "#test/utils/gameManager"; -import { SPLASH_ONLY } from "#test/utils/testUtils"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { SPLASH_ONLY } from "#test/utils/testUtils"; +import { TurnInitPhase } from "#app/phases/turn-init-phase"; describe("Moves - Haze", () => { describe("integration tests", () => { @@ -37,44 +36,28 @@ describe("Moves - Haze", () => { game.override.ability(Abilities.NONE); }); - it("Uses Swords Dance to raise own ATK by 2, Charm to lower enemy ATK by 2, player uses Haze to clear all stat changes", { timeout: 10000 }, async () => { + it("should reset all stat changes of all Pokemon on field", { timeout: 10000 }, async () => { await game.startBattle([Species.RATTATA]); const user = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; - expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0); - expect(enemy.summonData.battleStats[BattleStat.ATK]).toBe(0); + + expect(user.getStatStage(Stat.ATK)).toBe(0); + expect(enemy.getStatStage(Stat.ATK)).toBe(0); game.move.select(Moves.SWORDS_DANCE); await game.phaseInterceptor.to(TurnInitPhase); game.move.select(Moves.CHARM); await game.phaseInterceptor.to(TurnInitPhase); - const userAtkBefore = user.summonData.battleStats[BattleStat.ATK]; - const enemyAtkBefore = enemy.summonData.battleStats[BattleStat.ATK]; - expect(userAtkBefore).toBe(2); - expect(enemyAtkBefore).toBe(-2); + + expect(user.getStatStage(Stat.ATK)).toBe(2); + expect(enemy.getStatStage(Stat.ATK)).toBe(-2); game.move.select(Moves.HAZE); await game.phaseInterceptor.to(TurnInitPhase); - expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0); - expect(enemy.summonData.battleStats[BattleStat.ATK]).toBe(0); - }); - it("Uses Swords Dance to raise own ATK by 2, Charm to lower enemy ATK by 2, enemy uses Haze to clear all stat changes", { timeout: 10000 }, async () => { - game.override.enemyMoveset([Moves.HAZE, Moves.HAZE, Moves.HAZE, Moves.HAZE]); - await game.startBattle([Species.SHUCKLE]); // Shuckle for slower Swords Dance on first turn so Haze doesn't affect it. - const user = game.scene.getPlayerPokemon()!; - expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0); - - game.move.select(Moves.SWORDS_DANCE); - await game.phaseInterceptor.to(TurnInitPhase); - - const userAtkBefore = user.summonData.battleStats[BattleStat.ATK]; - expect(userAtkBefore).toBe(2); - - game.move.select(Moves.SPLASH); - await game.phaseInterceptor.to(MoveEndPhase); - expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0); + expect(user.getStatStage(Stat.ATK)).toBe(0); + expect(enemy.getStatStage(Stat.ATK)).toBe(0); }); }); }); diff --git a/src/test/moves/lash_out.test.ts b/src/test/moves/lash_out.test.ts index 78f4b712cf0..74d9fcd66c0 100644 --- a/src/test/moves/lash_out.test.ts +++ b/src/test/moves/lash_out.test.ts @@ -39,7 +39,7 @@ describe("Moves - Lash Out", () => { }); - it("should deal double damage if the user's stats were lowered this turn", async () => { + it("should deal double damage if the user's stat stages were lowered this turn", async () => { vi.spyOn(allMoves[Moves.LASH_OUT], "calculateBattlePower"); await game.classicMode.startBattle(); diff --git a/src/test/moves/make_it_rain.test.ts b/src/test/moves/make_it_rain.test.ts index 0af7763f175..e41472d7561 100644 --- a/src/test/moves/make_it_rain.test.ts +++ b/src/test/moves/make_it_rain.test.ts @@ -1,13 +1,13 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { MoveEndPhase } from "#app/phases/move-end-phase"; -import { StatChangePhase } from "#app/phases/stat-change-phase"; +import { Stat } from "#enums/stat"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; -import { SPLASH_ONLY } from "#test/utils/testUtils"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { SPLASH_ONLY } from "#test/utils/testUtils"; +import { MoveEndPhase } from "#app/phases/move-end-phase"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; const TIMEOUT = 20 * 1000; @@ -36,17 +36,17 @@ describe("Moves - Make It Rain", () => { game.override.enemyLevel(100); }); - it("should only reduce Sp. Atk. once in a double battle", async () => { + it("should only lower SPATK stat stage by 1 once in a double battle", async () => { await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]); - const playerPokemon = game.scene.getPlayerField(); + const playerPokemon = game.scene.getPlayerPokemon()!; game.move.select(Moves.MAKE_IT_RAIN); game.move.select(Moves.SPLASH, 1); await game.phaseInterceptor.to(MoveEndPhase); - expect(playerPokemon[0].summonData.battleStats[BattleStat.SPATK]).toBe(-1); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1); }, TIMEOUT); it("should apply effects even if the target faints", async () => { @@ -60,10 +60,10 @@ describe("Moves - Make It Rain", () => { game.move.select(Moves.MAKE_IT_RAIN); - await game.phaseInterceptor.to(StatChangePhase); + await game.phaseInterceptor.to(StatStageChangePhase); expect(enemyPokemon.isFainted()).toBe(true); - expect(playerPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(-1); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1); }, TIMEOUT); it("should reduce Sp. Atk. once after KOing two enemies", async () => { @@ -71,22 +71,22 @@ describe("Moves - Make It Rain", () => { await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]); - const playerPokemon = game.scene.getPlayerField(); + const playerPokemon = game.scene.getPlayerPokemon()!; const enemyPokemon = game.scene.getEnemyField(); game.move.select(Moves.MAKE_IT_RAIN); game.move.select(Moves.SPLASH, 1); - await game.phaseInterceptor.to(StatChangePhase); + await game.phaseInterceptor.to(StatStageChangePhase); enemyPokemon.forEach(p => expect(p.isFainted()).toBe(true)); - expect(playerPokemon[0].summonData.battleStats[BattleStat.SPATK]).toBe(-1); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1); }, TIMEOUT); - it("should reduce Sp. Atk if it only hits the second target", async () => { + it("should lower SPATK stat stage by 1 if it only hits the second target", async () => { await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]); - const playerPokemon = game.scene.getPlayerField(); + const playerPokemon = game.scene.getPlayerPokemon()!; game.move.select(Moves.MAKE_IT_RAIN); game.move.select(Moves.SPLASH, 1); @@ -96,6 +96,6 @@ describe("Moves - Make It Rain", () => { await game.phaseInterceptor.to(MoveEndPhase); - expect(playerPokemon[0].summonData.battleStats[BattleStat.SPATK]).toBe(-1); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1); }, TIMEOUT); }); diff --git a/src/test/moves/mat_block.test.ts b/src/test/moves/mat_block.test.ts index 29a97806242..4a95985eb92 100644 --- a/src/test/moves/mat_block.test.ts +++ b/src/test/moves/mat_block.test.ts @@ -1,13 +1,13 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { BerryPhase } from "#app/phases/berry-phase"; -import { CommandPhase } from "#app/phases/command-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; -import { Abilities } from "#enums/abilities"; -import { Moves } from "#enums/moves"; -import { Species } from "#enums/species"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; import GameManager from "../utils/gameManager"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Stat } from "#enums/stat"; +import { BerryPhase } from "#app/phases/berry-phase"; +import { CommandPhase } from "#app/phases/command-phase"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; const TIMEOUT = 20 * 1000; @@ -76,7 +76,7 @@ describe("Moves - Mat Block", () => { await game.phaseInterceptor.to(BerryPhase, false); - leadPokemon.forEach(p => expect(p.summonData.battleStats[BattleStat.ATK]).toBe(-2)); + leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(-2)); }, TIMEOUT ); diff --git a/src/test/moves/octolock.test.ts b/src/test/moves/octolock.test.ts index 34dad13b0d9..c86906ea240 100644 --- a/src/test/moves/octolock.test.ts +++ b/src/test/moves/octolock.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { TrappedTag } from "#app/data/battler-tags"; import { CommandPhase } from "#app/phases/command-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; @@ -12,110 +12,106 @@ import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Moves - Octolock", () => { - describe("integration tests", () => { - let phaserGame: Phaser.Game; - let game: GameManager; + 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.battleType("single"); - - game.override.enemySpecies(Species.RATTATA); - game.override.enemyMoveset(SPLASH_ONLY); - game.override.enemyAbility(Abilities.BALL_FETCH); - - game.override.startingLevel(2000); - game.override.moveset([Moves.OCTOLOCK, Moves.SPLASH]); - game.override.ability(Abilities.BALL_FETCH); - }); - - it("Reduces DEf and SPDEF by 1 each turn", { timeout: 10000 }, async () => { - await game.startBattle([Species.GRAPPLOCT]); - - const enemyPokemon = game.scene.getEnemyField(); - - // use Octolock and advance to init phase of next turn to check for stat changes - game.move.select(Moves.OCTOLOCK); - await game.phaseInterceptor.to(TurnInitPhase); - - expect(enemyPokemon[0].summonData.battleStats[BattleStat.DEF]).toBe(-1); - expect(enemyPokemon[0].summonData.battleStats[BattleStat.SPDEF]).toBe(-1); - - // take a second turn to make sure stat changes occur again - await game.phaseInterceptor.to(CommandPhase); - game.move.select(Moves.SPLASH); - - await game.phaseInterceptor.to(TurnInitPhase); - expect(enemyPokemon[0].summonData.battleStats[BattleStat.DEF]).toBe(-2); - expect(enemyPokemon[0].summonData.battleStats[BattleStat.SPDEF]).toBe(-2); - }); - - it("If target pokemon has Big Pecks, Octolock should only reduce spdef by 1", { timeout: 10000 }, async () => { - game.override.enemyAbility(Abilities.BIG_PECKS); - await game.startBattle([Species.GRAPPLOCT]); - - const enemyPokemon = game.scene.getEnemyField(); - - // use Octolock and advance to init phase of next turn to check for stat changes - game.move.select(Moves.OCTOLOCK); - await game.phaseInterceptor.to(TurnInitPhase); - - expect(enemyPokemon[0].summonData.battleStats[BattleStat.DEF]).toBe(0); - expect(enemyPokemon[0].summonData.battleStats[BattleStat.SPDEF]).toBe(-1); - }); - - it("If target pokemon has White Smoke, Octolock should not reduce any stats", { timeout: 10000 }, async () => { - game.override.enemyAbility(Abilities.WHITE_SMOKE); - await game.startBattle([Species.GRAPPLOCT]); - - const enemyPokemon = game.scene.getEnemyField(); - - // use Octolock and advance to init phase of next turn to check for stat changes - game.move.select(Moves.OCTOLOCK); - await game.phaseInterceptor.to(TurnInitPhase); - - expect(enemyPokemon[0].summonData.battleStats[BattleStat.DEF]).toBe(0); - expect(enemyPokemon[0].summonData.battleStats[BattleStat.SPDEF]).toBe(0); - }); - - it("If target pokemon has Clear Body, Octolock should not reduce any stats", { timeout: 10000 }, async () => { - game.override.enemyAbility(Abilities.CLEAR_BODY); - await game.startBattle([Species.GRAPPLOCT]); - - const enemyPokemon = game.scene.getEnemyField(); - - // use Octolock and advance to init phase of next turn to check for stat changes - game.move.select(Moves.OCTOLOCK); - await game.phaseInterceptor.to(TurnInitPhase); - - expect(enemyPokemon[0].summonData.battleStats[BattleStat.DEF]).toBe(0); - expect(enemyPokemon[0].summonData.battleStats[BattleStat.SPDEF]).toBe(0); - }); - - it("Traps the target pokemon", { timeout: 10000 }, async () => { - await game.startBattle([Species.GRAPPLOCT]); - - const enemyPokemon = game.scene.getEnemyField(); - - // before Octolock - enemy should not be trapped - expect(enemyPokemon[0].findTag(t => t instanceof TrappedTag)).toBeUndefined(); - - game.move.select(Moves.OCTOLOCK); - - // after Octolock - enemy should be trapped - await game.phaseInterceptor.to(MoveEndPhase); - expect(enemyPokemon[0].findTag(t => t instanceof TrappedTag)).toBeDefined(); + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, }); }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override.battleType("single") + .enemySpecies(Species.RATTATA) + .enemyMoveset(SPLASH_ONLY) + .enemyAbility(Abilities.BALL_FETCH) + .startingLevel(2000) + .moveset([ Moves.OCTOLOCK, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH); + }); + + it("lowers DEF and SPDEF stat stages of the target Pokemon by 1 each turn", { timeout: 10000 }, async () => { + await game.classicMode.startBattle([ Species.GRAPPLOCT ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + // use Octolock and advance to init phase of next turn to check for stat changes + game.move.select(Moves.OCTOLOCK); + await game.phaseInterceptor.to(TurnInitPhase); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1); + expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1); + + // take a second turn to make sure stat changes occur again + await game.phaseInterceptor.to(CommandPhase); + game.move.select(Moves.SPLASH); + + await game.phaseInterceptor.to(TurnInitPhase); + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-2); + expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-2); + }); + + it("if target pokemon has BIG_PECKS, should only lower SPDEF stat stage by 1", { timeout: 10000 }, async () => { + game.override.enemyAbility(Abilities.BIG_PECKS); + await game.classicMode.startBattle([ Species.GRAPPLOCT ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + // use Octolock and advance to init phase of next turn to check for stat changes + game.move.select(Moves.OCTOLOCK); + await game.phaseInterceptor.to(TurnInitPhase); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1); + }); + + it("if target pokemon has WHITE_SMOKE, should not reduce any stat stages", { timeout: 10000 }, async () => { + game.override.enemyAbility(Abilities.WHITE_SMOKE); + await game.classicMode.startBattle([ Species.GRAPPLOCT ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + // use Octolock and advance to init phase of next turn to check for stat changes + game.move.select(Moves.OCTOLOCK); + await game.phaseInterceptor.to(TurnInitPhase); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0); + }); + + it("if target pokemon has CLEAR_BODY, should not reduce any stat stages", { timeout: 10000 }, async () => { + game.override.enemyAbility(Abilities.CLEAR_BODY); + await game.classicMode.startBattle([ Species.GRAPPLOCT ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + // use Octolock and advance to init phase of next turn to check for stat changes + game.move.select(Moves.OCTOLOCK); + await game.phaseInterceptor.to(TurnInitPhase); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0); + }); + + it("traps the target pokemon", { timeout: 10000 }, async () => { + await game.classicMode.startBattle([ Species.GRAPPLOCT ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + // before Octolock - enemy should not be trapped + expect(enemyPokemon.findTag(t => t instanceof TrappedTag)).toBeUndefined(); + + game.move.select(Moves.OCTOLOCK); + + // after Octolock - enemy should be trapped + await game.phaseInterceptor.to(MoveEndPhase); + expect(enemyPokemon.findTag(t => t instanceof TrappedTag)).toBeDefined(); + }); }); diff --git a/src/test/moves/parting_shot.test.ts b/src/test/moves/parting_shot.test.ts index 7c2ca3f334c..d9535ca6482 100644 --- a/src/test/moves/parting_shot.test.ts +++ b/src/test/moves/parting_shot.test.ts @@ -1,14 +1,14 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { BerryPhase } from "#app/phases/berry-phase"; -import { FaintPhase } from "#app/phases/faint-phase"; -import { MessagePhase } from "#app/phases/message-phase"; -import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, test } from "vitest"; import GameManager from "../utils/gameManager"; +import { Stat } from "#enums/stat"; +import { BerryPhase } from "#app/phases/berry-phase"; +import { FaintPhase } from "#app/phases/faint-phase"; +import { MessagePhase } from "#app/phases/message-phase"; +import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { SPLASH_ONLY } from "../utils/testUtils"; const TIMEOUT = 20 * 1000; @@ -51,9 +51,8 @@ describe("Moves - Parting Shot", () => { game.move.select(Moves.PARTING_SHOT); await game.phaseInterceptor.to(BerryPhase, false); - const battleStatsOpponent = enemyPokemon.summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(0); - expect(battleStatsOpponent[BattleStat.SPATK]).toBe(0); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW); }, TIMEOUT ); @@ -72,9 +71,8 @@ describe("Moves - Parting Shot", () => { game.move.select(Moves.PARTING_SHOT); await game.phaseInterceptor.to(BerryPhase, false); - const battleStatsOpponent = enemyPokemon.summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(0); - expect(battleStatsOpponent[BattleStat.SPATK]).toBe(0); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW); }, TIMEOUT ); @@ -108,16 +106,15 @@ describe("Moves - Parting Shot", () => { const enemyPokemon = game.scene.getEnemyPokemon()!; expect(enemyPokemon).toBeDefined(); - const battleStatsOpponent = enemyPokemon.summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-6); - expect(battleStatsOpponent[BattleStat.SPATK]).toBe(-6); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-6); + expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-6); // now parting shot should fail game.move.select(Moves.PARTING_SHOT); await game.phaseInterceptor.to(BerryPhase, false); - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-6); - expect(battleStatsOpponent[BattleStat.SPATK]).toBe(-6); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-6); + expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-6); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW); }, TIMEOUT ); @@ -137,9 +134,8 @@ describe("Moves - Parting Shot", () => { game.move.select(Moves.PARTING_SHOT); await game.phaseInterceptor.to(BerryPhase, false); - const battleStatsOpponent = enemyPokemon.summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(0); - expect(battleStatsOpponent[BattleStat.SPATK]).toBe(0); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW); }, TIMEOUT ); @@ -158,9 +154,8 @@ describe("Moves - Parting Shot", () => { game.move.select(Moves.PARTING_SHOT); await game.phaseInterceptor.to(BerryPhase, false); - const battleStatsOpponent = enemyPokemon.summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(0); - expect(battleStatsOpponent[BattleStat.SPATK]).toBe(0); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW); }, TIMEOUT ); @@ -176,9 +171,8 @@ describe("Moves - Parting Shot", () => { game.move.select(Moves.PARTING_SHOT); await game.phaseInterceptor.to(BerryPhase, false); - const battleStatsOpponent = enemyPokemon.summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); - expect(battleStatsOpponent[BattleStat.SPATK]).toBe(-1); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW); }, TIMEOUT ); @@ -199,9 +193,9 @@ describe("Moves - Parting Shot", () => { game.move.select(Moves.PARTING_SHOT); await game.phaseInterceptor.to(BerryPhase, false); - const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(0); - expect(battleStatsOpponent[BattleStat.SPATK]).toBe(0); + const enemyPokemon = game.scene.getEnemyPokemon()!; + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MEOWTH); }, TIMEOUT ); diff --git a/src/test/moves/power_split.test.ts b/src/test/moves/power_split.test.ts new file mode 100644 index 00000000000..a532a90a54d --- /dev/null +++ b/src/test/moves/power_split.test.ts @@ -0,0 +1,82 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import { Species } from "#enums/species"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { Moves } from "#enums/moves"; +import { Stat } from "#enums/stat"; +import { Abilities } from "#enums/abilities"; +import { SPLASH_ONLY } from "../utils/testUtils"; + +describe("Moves - Power Split", () => { + 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 + .battleType("single") + .enemyAbility(Abilities.NONE) + .enemySpecies(Species.MEW) + .enemyLevel(200) + .moveset([ Moves.POWER_SPLIT ]) + .ability(Abilities.NONE); + }); + + it("should average the user's ATK and SPATK stats with those of the target", async () => { + game.override.enemyMoveset(SPLASH_ONLY); + await game.startBattle([ + Species.INDEEDEE + ]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + const avgAtk = Math.floor((player.getStat(Stat.ATK, false) + enemy.getStat(Stat.ATK, false)) / 2); + const avgSpAtk = Math.floor((player.getStat(Stat.SPATK, false) + enemy.getStat(Stat.SPATK, false)) / 2); + + game.move.select(Moves.POWER_SPLIT); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStat(Stat.ATK, false)).toBe(avgAtk); + expect(enemy.getStat(Stat.ATK, false)).toBe(avgAtk); + + expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); + expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk); + }, 20000); + + it("should be idempotent", async () => { + game.override.enemyMoveset(new Array(4).fill(Moves.POWER_SPLIT)); + await game.startBattle([ + Species.INDEEDEE + ]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + const avgAtk = Math.floor((player.getStat(Stat.ATK, false) + enemy.getStat(Stat.ATK, false)) / 2); + const avgSpAtk = Math.floor((player.getStat(Stat.SPATK, false) + enemy.getStat(Stat.SPATK, false)) / 2); + + game.move.select(Moves.POWER_SPLIT); + await game.phaseInterceptor.to(TurnEndPhase); + + game.move.select(Moves.POWER_SPLIT); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStat(Stat.ATK, false)).toBe(avgAtk); + expect(enemy.getStat(Stat.ATK, false)).toBe(avgAtk); + + expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); + expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk); + }, 20000); +}); diff --git a/src/test/moves/power_swap.test.ts b/src/test/moves/power_swap.test.ts new file mode 100644 index 00000000000..f1efeaa3af3 --- /dev/null +++ b/src/test/moves/power_swap.test.ts @@ -0,0 +1,62 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import { Species } from "#enums/species"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { Moves } from "#enums/moves"; +import { Stat } from "#enums/stat"; +import { Abilities } from "#enums/abilities"; +import { MoveEndPhase } from "#app/phases/move-end-phase"; + +describe("Moves - Power Swap", () => { + 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 + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(new Array(4).fill(Moves.SHELL_SMASH)) + .enemySpecies(Species.MEW) + .enemyLevel(200) + .moveset([ Moves.POWER_SWAP ]) + .ability(Abilities.NONE); + }); + + it("should swap the user's ATK AND SPATK stat stages with the target's", async () => { + await game.startBattle([ + Species.INDEEDEE + ]); + + // Should start with no stat stages + const player = game.scene.getPlayerPokemon()!; + // After Shell Smash, should have +2 in ATK and SPATK, -1 in DEF and SPDEF + const enemy = game.scene.getEnemyPokemon()!; + game.move.select(Moves.POWER_SWAP); + + await game.phaseInterceptor.to(MoveEndPhase); + + expect(player.getStatStage(Stat.ATK)).toBe(0); + expect(player.getStatStage(Stat.SPATK)).toBe(0); + expect(enemy.getStatStage(Stat.ATK)).toBe(2); + expect(enemy.getStatStage(Stat.SPATK)).toBe(2); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.ATK)).toBe(2); + expect(player.getStatStage(Stat.SPATK)).toBe(2); + expect(enemy.getStatStage(Stat.ATK)).toBe(0); + expect(enemy.getStatStage(Stat.SPATK)).toBe(0); + }, 20000); +}); diff --git a/src/test/moves/protect.test.ts b/src/test/moves/protect.test.ts index 3fd51f4bc93..d792f586a37 100644 --- a/src/test/moves/protect.test.ts +++ b/src/test/moves/protect.test.ts @@ -1,13 +1,13 @@ -import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; -import { BattleStat } from "#app/data/battle-stat"; -import { allMoves } from "#app/data/move"; -import { BerryPhase } from "#app/phases/berry-phase"; -import { Abilities } from "#enums/abilities"; -import { Moves } from "#enums/moves"; -import { Species } from "#enums/species"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; import GameManager from "../utils/gameManager"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Stat } from "#enums/stat"; +import { allMoves } from "#app/data/move"; +import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; +import { BerryPhase } from "#app/phases/berry-phase"; const TIMEOUT = 20 * 1000; @@ -87,7 +87,7 @@ describe("Moves - Protect", () => { await game.phaseInterceptor.to(BerryPhase, false); - expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0); + expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0); }, TIMEOUT ); diff --git a/src/test/moves/quick_guard.test.ts b/src/test/moves/quick_guard.test.ts index 26d9a74e9fd..25f98f8fa61 100644 --- a/src/test/moves/quick_guard.test.ts +++ b/src/test/moves/quick_guard.test.ts @@ -1,12 +1,12 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { BerryPhase } from "#app/phases/berry-phase"; -import { CommandPhase } from "#app/phases/command-phase"; -import { Abilities } from "#enums/abilities"; -import { Moves } from "#enums/moves"; -import { Species } from "#enums/species"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; import GameManager from "../utils/gameManager"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Stat } from "#enums/stat"; +import { BerryPhase } from "#app/phases/berry-phase"; +import { CommandPhase } from "#app/phases/command-phase"; const TIMEOUT = 20 * 1000; @@ -76,7 +76,7 @@ describe("Moves - Quick Guard", () => { await game.phaseInterceptor.to(BerryPhase, false); - leadPokemon.forEach(p => expect(p.summonData.battleStats[BattleStat.ATK]).toBe(0)); + leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0)); }, TIMEOUT ); diff --git a/src/test/moves/speed_swap.test.ts b/src/test/moves/speed_swap.test.ts new file mode 100644 index 00000000000..131d506792b --- /dev/null +++ b/src/test/moves/speed_swap.test.ts @@ -0,0 +1,54 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import { Species } from "#enums/species"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { Moves } from "#enums/moves"; +import { Stat } from "#enums/stat"; +import { Abilities } from "#enums/abilities"; +import { SPLASH_ONLY } from "../utils/testUtils"; + +describe("Moves - Speed Swap", () => { + 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 + .battleType("single") + .enemyAbility(Abilities.NONE) + .enemyMoveset(SPLASH_ONLY) + .enemySpecies(Species.MEW) + .enemyLevel(200) + .moveset([ Moves.SPEED_SWAP ]) + .ability(Abilities.NONE); + }); + + it("should swap the user's SPD and the target's SPD stats", async () => { + await game.startBattle([ + Species.INDEEDEE + ]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + const playerSpd = player.getStat(Stat.SPD, false); + const enemySpd = enemy.getStat(Stat.SPD, false); + + game.move.select(Moves.SPEED_SWAP); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStat(Stat.SPD, false)).toBe(enemySpd); + expect(enemy.getStat(Stat.SPD, false)).toBe(playerSpd); + }, 20000); +}); diff --git a/src/test/moves/spit_up.test.ts b/src/test/moves/spit_up.test.ts index ab47e65d653..f88791efb74 100644 --- a/src/test/moves/spit_up.test.ts +++ b/src/test/moves/spit_up.test.ts @@ -1,22 +1,24 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { StockpilingTag } from "#app/data/battler-tags"; import { allMoves } from "#app/data/move"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { MoveResult, TurnMove } from "#app/field/pokemon"; -import { MovePhase } from "#app/phases/move-phase"; -import { TurnInitPhase } from "#app/phases/turn-init-phase"; +import GameManager from "#test/utils/gameManager"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import GameManager from "#test/utils/gameManager"; -import { SPLASH_ONLY } from "#test/utils/testUtils"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { SPLASH_ONLY } from "#test/utils/testUtils"; +import { MovePhase } from "#app/phases/move-phase"; +import { TurnInitPhase } from "#app/phases/turn-init-phase"; describe("Moves - Spit Up", () => { let phaserGame: Phaser.Game; let game: GameManager; + const spitUp = allMoves[Moves.SPIT_UP]; + beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); }); @@ -35,8 +37,10 @@ describe("Moves - Spit Up", () => { game.override.enemyAbility(Abilities.NONE); game.override.enemyLevel(2000); - game.override.moveset([Moves.SPIT_UP, Moves.SPIT_UP, Moves.SPIT_UP, Moves.SPIT_UP]); + game.override.moveset(new Array(4).fill(spitUp.id)); game.override.ability(Abilities.NONE); + + vi.spyOn(spitUp, "calculateBattlePower"); }); describe("consumes all stockpile stacks to deal damage (scaling with stacks)", () => { @@ -53,13 +57,11 @@ describe("Moves - Spit Up", () => { expect(stockpilingTag).toBeDefined(); expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup); - vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower"); - game.move.select(Moves.SPIT_UP); await game.phaseInterceptor.to(TurnInitPhase); - expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveBeenCalledOnce(); - expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveReturnedWith(expectedPower); + expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce(); + expect(spitUp.calculateBattlePower).toHaveReturnedWith(expectedPower); expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); }); @@ -78,13 +80,11 @@ describe("Moves - Spit Up", () => { expect(stockpilingTag).toBeDefined(); expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup); - vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower"); - game.move.select(Moves.SPIT_UP); await game.phaseInterceptor.to(TurnInitPhase); - expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveBeenCalledOnce(); - expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveReturnedWith(expectedPower); + expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce(); + expect(spitUp.calculateBattlePower).toHaveReturnedWith(expectedPower); expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); }); @@ -104,13 +104,11 @@ describe("Moves - Spit Up", () => { expect(stockpilingTag).toBeDefined(); expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup); - vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower"); - game.move.select(Moves.SPIT_UP); await game.phaseInterceptor.to(TurnInitPhase); - expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveBeenCalledOnce(); - expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveReturnedWith(expectedPower); + expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce(); + expect(spitUp.calculateBattlePower).toHaveReturnedWith(expectedPower); expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); }); @@ -124,14 +122,12 @@ describe("Moves - Spit Up", () => { const stockpilingTag = pokemon.getTag(StockpilingTag)!; expect(stockpilingTag).toBeUndefined(); - vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower"); - game.move.select(Moves.SPIT_UP); await game.phaseInterceptor.to(TurnInitPhase); expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SPIT_UP, result: MoveResult.FAIL }); - expect(allMoves[Moves.SPIT_UP].calculateBattlePower).not.toHaveBeenCalled(); + expect(spitUp.calculateBattlePower).not.toHaveBeenCalled(); }); describe("restores stat boosts granted by stacks", () => { @@ -144,22 +140,20 @@ describe("Moves - Spit Up", () => { const stockpilingTag = pokemon.getTag(StockpilingTag)!; expect(stockpilingTag).toBeDefined(); - vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower"); - game.move.select(Moves.SPIT_UP); await game.phaseInterceptor.to(MovePhase); - expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(1); - expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(1); + expect(pokemon.getStatStage(Stat.DEF)).toBe(1); + expect(pokemon.getStatStage(Stat.SPDEF)).toBe(1); await game.phaseInterceptor.to(TurnInitPhase); expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS }); - expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveBeenCalledOnce(); + expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce(); - expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(0); - expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(0); + expect(pokemon.getStatStage(Stat.DEF)).toBe(0); + expect(pokemon.getStatStage(Stat.SPDEF)).toBe(0); expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); }); @@ -175,26 +169,19 @@ describe("Moves - Spit Up", () => { // for the sake of simplicity (and because other tests cover the setup), set boost amounts directly stockpilingTag.statChangeCounts = { - [BattleStat.DEF]: -1, - [BattleStat.SPDEF]: 2, + [Stat.DEF]: -1, + [Stat.SPDEF]: 2, }; - expect(stockpilingTag.statChangeCounts).toMatchObject({ - [BattleStat.DEF]: -1, - [BattleStat.SPDEF]: 2, - }); - - vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower"); - game.move.select(Moves.SPIT_UP); await game.phaseInterceptor.to(TurnInitPhase); expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS }); - expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveBeenCalledOnce(); + expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce(); - expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(1); - expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(-2); + expect(pokemon.getStatStage(Stat.DEF)).toBe(1); + expect(pokemon.getStatStage(Stat.SPDEF)).toBe(-2); expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); }); diff --git a/src/test/moves/spotlight.test.ts b/src/test/moves/spotlight.test.ts index e5f4719d1d3..e4dc8815f6d 100644 --- a/src/test/moves/spotlight.test.ts +++ b/src/test/moves/spotlight.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Stat } from "#app/data/pokemon-stat"; +import { Stat } from "#enums/stat"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -65,7 +65,7 @@ describe("Moves - Spotlight", () => { * Spotlight will target the slower enemy. In this situation without Spotlight being used, * the faster enemy would normally end up with the Center of Attention tag. */ - enemyPokemon.sort((a, b) => b.getBattleStat(Stat.SPD) - a.getBattleStat(Stat.SPD)); + enemyPokemon.sort((a, b) => b.getEffectiveStat(Stat.SPD) - a.getEffectiveStat(Stat.SPD)); const spotTarget = enemyPokemon[1].getBattlerIndex(); const attackTarget = enemyPokemon[0].getBattlerIndex(); diff --git a/src/test/moves/stockpile.test.ts b/src/test/moves/stockpile.test.ts index b1941b9f9b3..d57768d0ffd 100644 --- a/src/test/moves/stockpile.test.ts +++ b/src/test/moves/stockpile.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { StockpilingTag } from "#app/data/battler-tags"; import { MoveResult, TurnMove } from "#app/field/pokemon"; import { CommandPhase } from "#app/phases/command-phase"; @@ -38,7 +38,7 @@ describe("Moves - Stockpile", () => { game.override.ability(Abilities.NONE); }); - it("Gains a stockpile stack and increases DEF and SPDEF by 1 on each use, fails at max stacks (3)", { timeout: 10000 }, async () => { + it("gains a stockpile stack and raises user's DEF and SPDEF stat stages by 1 on each use, fails at max stacks (3)", { timeout: 10000 }, async () => { await game.startBattle([Species.ABOMASNOW]); const user = game.scene.getPlayerPokemon()!; @@ -47,8 +47,8 @@ describe("Moves - Stockpile", () => { // we just have to know that they're implemented as a BattlerTag. expect(user.getTag(StockpilingTag)).toBeUndefined(); - expect(user.summonData.battleStats[BattleStat.DEF]).toBe(0); - expect(user.summonData.battleStats[BattleStat.SPDEF]).toBe(0); + expect(user.getStatStage(Stat.DEF)).toBe(0); + expect(user.getStatStage(Stat.SPDEF)).toBe(0); // use Stockpile four times for (let i = 0; i < 4; i++) { @@ -60,18 +60,16 @@ describe("Moves - Stockpile", () => { await game.phaseInterceptor.to(TurnInitPhase); const stockpilingTag = user.getTag(StockpilingTag)!; - const def = user.summonData.battleStats[BattleStat.DEF]; - const spdef = user.summonData.battleStats[BattleStat.SPDEF]; if (i < 3) { // first three uses should behave normally - expect(def).toBe(i + 1); - expect(spdef).toBe(i + 1); + expect(user.getStatStage(Stat.DEF)).toBe(i + 1); + expect(user.getStatStage(Stat.SPDEF)).toBe(i + 1); expect(stockpilingTag).toBeDefined(); expect(stockpilingTag.stockpiledCount).toBe(i + 1); } else { // fourth should have failed - expect(def).toBe(3); - expect(spdef).toBe(3); + expect(user.getStatStage(Stat.DEF)).toBe(3); + expect(user.getStatStage(Stat.SPDEF)).toBe(3); expect(stockpilingTag).toBeDefined(); expect(stockpilingTag.stockpiledCount).toBe(3); expect(user.getMoveHistory().at(-1)).toMatchObject({ result: MoveResult.FAIL, move: Moves.STOCKPILE }); @@ -79,17 +77,17 @@ describe("Moves - Stockpile", () => { } }); - it("Gains a stockpile stack even if DEF and SPDEF are at +6", { timeout: 10000 }, async () => { + it("gains a stockpile stack even if user's DEF and SPDEF stat stages are at +6", { timeout: 10000 }, async () => { await game.startBattle([Species.ABOMASNOW]); const user = game.scene.getPlayerPokemon()!; - user.summonData.battleStats[BattleStat.DEF] = 6; - user.summonData.battleStats[BattleStat.SPDEF] = 6; + user.setStatStage(Stat.DEF, 6); + user.setStatStage(Stat.SPDEF, 6); expect(user.getTag(StockpilingTag)).toBeUndefined(); - expect(user.summonData.battleStats[BattleStat.DEF]).toBe(6); - expect(user.summonData.battleStats[BattleStat.SPDEF]).toBe(6); + expect(user.getStatStage(Stat.DEF)).toBe(6); + expect(user.getStatStage(Stat.SPDEF)).toBe(6); game.move.select(Moves.STOCKPILE); await game.phaseInterceptor.to(TurnInitPhase); @@ -97,8 +95,8 @@ describe("Moves - Stockpile", () => { const stockpilingTag = user.getTag(StockpilingTag)!; expect(stockpilingTag).toBeDefined(); expect(stockpilingTag.stockpiledCount).toBe(1); - expect(user.summonData.battleStats[BattleStat.DEF]).toBe(6); - expect(user.summonData.battleStats[BattleStat.SPDEF]).toBe(6); + expect(user.getStatStage(Stat.DEF)).toBe(6); + expect(user.getStatStage(Stat.SPDEF)).toBe(6); // do it again, just for good measure await game.phaseInterceptor.to(CommandPhase); @@ -109,8 +107,8 @@ describe("Moves - Stockpile", () => { const stockpilingTagAgain = user.getTag(StockpilingTag)!; expect(stockpilingTagAgain).toBeDefined(); expect(stockpilingTagAgain.stockpiledCount).toBe(2); - expect(user.summonData.battleStats[BattleStat.DEF]).toBe(6); - expect(user.summonData.battleStats[BattleStat.SPDEF]).toBe(6); + expect(user.getStatStage(Stat.DEF)).toBe(6); + expect(user.getStatStage(Stat.SPDEF)).toBe(6); }); }); }); diff --git a/src/test/moves/swallow.test.ts b/src/test/moves/swallow.test.ts index 202f25fee74..9cea7ae8dc9 100644 --- a/src/test/moves/swallow.test.ts +++ b/src/test/moves/swallow.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { StockpilingTag } from "#app/data/battler-tags"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { MoveResult, TurnMove } from "#app/field/pokemon"; @@ -138,7 +138,7 @@ describe("Moves - Swallow", () => { expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SWALLOW, result: MoveResult.FAIL }); }); - describe("restores stat boosts granted by stacks", () => { + describe("restores stat stage boosts granted by stacks", () => { it("decreases stats based on stored values (both boosts equal)", { timeout: 10000 }, async () => { await game.startBattle([Species.ABOMASNOW]); @@ -151,20 +151,20 @@ describe("Moves - Swallow", () => { game.move.select(Moves.SWALLOW); await game.phaseInterceptor.to(MovePhase); - expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(1); - expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(1); + expect(pokemon.getStatStage(Stat.DEF)).toBe(1); + expect(pokemon.getStatStage(Stat.SPDEF)).toBe(1); await game.phaseInterceptor.to(TurnInitPhase); expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SWALLOW, result: MoveResult.SUCCESS }); - expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(0); - expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(0); + expect(pokemon.getStatStage(Stat.DEF)).toBe(0); + expect(pokemon.getStatStage(Stat.SPDEF)).toBe(0); expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); }); - it("decreases stats based on stored values (different boosts)", { timeout: 10000 }, async () => { + it("lower stat stages based on stored values (different boosts)", { timeout: 10000 }, async () => { await game.startBattle([Species.ABOMASNOW]); const pokemon = game.scene.getPlayerPokemon()!; @@ -175,22 +175,18 @@ describe("Moves - Swallow", () => { // for the sake of simplicity (and because other tests cover the setup), set boost amounts directly stockpilingTag.statChangeCounts = { - [BattleStat.DEF]: -1, - [BattleStat.SPDEF]: 2, + [Stat.DEF]: -1, + [Stat.SPDEF]: 2, }; - expect(stockpilingTag.statChangeCounts).toMatchObject({ - [BattleStat.DEF]: -1, - [BattleStat.SPDEF]: 2, - }); - game.move.select(Moves.SWALLOW); + await game.phaseInterceptor.to(TurnInitPhase); expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ move: Moves.SWALLOW, result: MoveResult.SUCCESS }); - expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(1); - expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(-2); + expect(pokemon.getStatStage(Stat.DEF)).toBe(1); + expect(pokemon.getStatStage(Stat.SPDEF)).toBe(-2); expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); }); diff --git a/src/test/moves/tackle.test.ts b/src/test/moves/tackle.test.ts index 5eca9e344c8..b25c7524a1a 100644 --- a/src/test/moves/tackle.test.ts +++ b/src/test/moves/tackle.test.ts @@ -1,4 +1,4 @@ -import { Stat } from "#app/data/pokemon-stat"; +import { Stat } from "#enums/stat"; import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Moves } from "#enums/moves"; diff --git a/src/test/moves/tail_whip.test.ts b/src/test/moves/tail_whip.test.ts index 0a999fe1920..04730a04f7a 100644 --- a/src/test/moves/tail_whip.test.ts +++ b/src/test/moves/tail_whip.test.ts @@ -1,12 +1,13 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; -import { TurnInitPhase } from "#app/phases/turn-init-phase"; +import { Stat } from "#enums/stat"; +import GameManager from "#test/utils/gameManager"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { SPLASH_ONLY } from "../utils/testUtils"; +import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; +import { TurnInitPhase } from "#app/phases/turn-init-phase"; describe("Moves - Tail whip", () => { @@ -31,23 +32,23 @@ describe("Moves - Tail whip", () => { game.override.enemyAbility(Abilities.INSOMNIA); game.override.ability(Abilities.INSOMNIA); game.override.startingLevel(2000); - game.override.moveset([moveToUse]); - game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + game.override.moveset([ moveToUse ]); + game.override.enemyMoveset(SPLASH_ONLY); }); - it("TAIL_WHIP", async () => { + it("should lower DEF stat stage by 1", async() => { const moveToUse = Moves.TAIL_WHIP; await game.startBattle([ Species.MIGHTYENA, Species.MIGHTYENA, ]); - let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.DEF]).toBe(0); + const enemyPokemon = game.scene.getEnemyPokemon()!; + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); game.move.select(moveToUse); await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnInitPhase); - battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.DEF]).toBe(-1); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1); }, 20000); }); diff --git a/src/test/moves/tailwind.test.ts b/src/test/moves/tailwind.test.ts index 6b70122d08d..d158a9cce86 100644 --- a/src/test/moves/tailwind.test.ts +++ b/src/test/moves/tailwind.test.ts @@ -1,5 +1,5 @@ +import { Stat } from "#enums/stat"; import { ArenaTagSide } from "#app/data/arena-tag"; -import { Stat } from "#app/data/pokemon-stat"; import { ArenaTagType } from "#app/enums/arena-tag-type"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Moves } from "#enums/moves"; @@ -38,16 +38,16 @@ describe("Moves - Tailwind", () => { const magikarpSpd = magikarp.getStat(Stat.SPD); const meowthSpd = meowth.getStat(Stat.SPD); - expect(magikarp.getBattleStat(Stat.SPD)).equal(magikarpSpd); - expect(meowth.getBattleStat(Stat.SPD)).equal(meowthSpd); + expect(magikarp.getEffectiveStat(Stat.SPD)).equal(magikarpSpd); + expect(meowth.getEffectiveStat(Stat.SPD)).equal(meowthSpd); game.move.select(Moves.TAILWIND); game.move.select(Moves.SPLASH, 1); await game.phaseInterceptor.to(TurnEndPhase); - expect(magikarp.getBattleStat(Stat.SPD)).toBe(magikarpSpd * 2); - expect(meowth.getBattleStat(Stat.SPD)).toBe(meowthSpd * 2); + expect(magikarp.getEffectiveStat(Stat.SPD)).toBe(magikarpSpd * 2); + expect(meowth.getEffectiveStat(Stat.SPD)).toBe(meowthSpd * 2); expect(game.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.PLAYER)).toBeDefined(); }); @@ -86,8 +86,8 @@ describe("Moves - Tailwind", () => { const enemySpd = enemy.getStat(Stat.SPD); - expect(ally.getBattleStat(Stat.SPD)).equal(allySpd); - expect(enemy.getBattleStat(Stat.SPD)).equal(enemySpd); + expect(ally.getEffectiveStat(Stat.SPD)).equal(allySpd); + expect(enemy.getEffectiveStat(Stat.SPD)).equal(enemySpd); expect(game.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.PLAYER)).toBeUndefined(); expect(game.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.ENEMY)).toBeUndefined(); @@ -95,8 +95,8 @@ describe("Moves - Tailwind", () => { await game.phaseInterceptor.to(TurnEndPhase); - expect(ally.getBattleStat(Stat.SPD)).toBe(allySpd * 2); - expect(enemy.getBattleStat(Stat.SPD)).equal(enemySpd); + expect(ally.getEffectiveStat(Stat.SPD)).toBe(allySpd * 2); + expect(enemy.getEffectiveStat(Stat.SPD)).equal(enemySpd); expect(game.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.PLAYER)).toBeDefined(); expect(game.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.ENEMY)).toBeUndefined(); }); diff --git a/src/test/moves/tera_blast.test.ts b/src/test/moves/tera_blast.test.ts index bd7df8403d1..fa7a99adc14 100644 --- a/src/test/moves/tera_blast.test.ts +++ b/src/test/moves/tera_blast.test.ts @@ -1,9 +1,8 @@ import { BattlerIndex } from "#app/battle"; -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { allMoves } from "#app/data/move"; import { Type } from "#app/data/type"; import { Abilities } from "#app/enums/abilities"; -import { Stat } from "#app/enums/stat"; import { HitResult } from "#app/field/pokemon"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -112,7 +111,7 @@ describe("Moves - Tera Blast", () => { await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to("MoveEndPhase"); - expect(playerPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(-1); - expect(playerPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-1); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1); + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); }, 20000); }); diff --git a/src/test/moves/tidy_up.test.ts b/src/test/moves/tidy_up.test.ts index 1ef7933c114..5204b06106b 100644 --- a/src/test/moves/tidy_up.test.ts +++ b/src/test/moves/tidy_up.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { ArenaTagType } from "#app/enums/arena-tag-type"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; @@ -60,7 +60,6 @@ describe("Moves - Tidy Up", () => { game.move.select(Moves.TIDY_UP); await game.phaseInterceptor.to(MoveEndPhase); expect(game.scene.arena.getTag(ArenaTagType.STEALTH_ROCK)).toBeUndefined(); - }, 20000); it("toxic spikes are cleared", async () => { @@ -73,7 +72,6 @@ describe("Moves - Tidy Up", () => { game.move.select(Moves.TIDY_UP); await game.phaseInterceptor.to(MoveEndPhase); expect(game.scene.arena.getTag(ArenaTagType.TOXIC_SPIKES)).toBeUndefined(); - }, 20000); it("sticky webs are cleared", async () => { @@ -87,7 +85,6 @@ describe("Moves - Tidy Up", () => { game.move.select(Moves.TIDY_UP); await game.phaseInterceptor.to(MoveEndPhase); expect(game.scene.arena.getTag(ArenaTagType.STICKY_WEB)).toBeUndefined(); - }, 20000); it.skip("substitutes are cleared", async () => { @@ -101,22 +98,20 @@ describe("Moves - Tidy Up", () => { game.move.select(Moves.TIDY_UP); await game.phaseInterceptor.to(MoveEndPhase); // TODO: check for subs here once the move is implemented - }, 20000); it("user's stats are raised with no traps set", async () => { await game.startBattle(); - const player = game.scene.getPlayerPokemon()!.summonData.battleStats; - expect(player[BattleStat.ATK]).toBe(0); - expect(player[BattleStat.SPD]).toBe(0); + const playerPokemon = game.scene.getPlayerPokemon()!; + + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); game.move.select(Moves.TIDY_UP); await game.phaseInterceptor.to(TurnEndPhase); - expect(player[BattleStat.ATK]).toBe(+1); - expect(player[BattleStat.SPD]).toBe(+1); - + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); }, 20000); - }); diff --git a/src/test/moves/transform.test.ts b/src/test/moves/transform.test.ts new file mode 100644 index 00000000000..45769447e4d --- /dev/null +++ b/src/test/moves/transform.test.ts @@ -0,0 +1,101 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import { Species } from "#enums/species"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { Moves } from "#enums/moves"; +import { Stat, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; +import { Abilities } from "#enums/abilities"; +import { SPLASH_ONLY } from "../utils/testUtils"; + +// TODO: Add more tests once Transform is fully implemented +describe("Moves - Transform", () => { + 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 + .battleType("single") + .enemySpecies(Species.MEW) + .enemyLevel(200) + .enemyAbility(Abilities.BEAST_BOOST) + .enemyPassiveAbility(Abilities.BALL_FETCH) + .enemyMoveset(SPLASH_ONLY) + .ability(Abilities.INTIMIDATE) + .moveset([ Moves.TRANSFORM ]); + }); + + it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => { + await game.startBattle([ + Species.DITTO + ]); + + game.move.select(Moves.TRANSFORM); + await game.phaseInterceptor.to(TurnEndPhase); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + expect(player.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId); + expect(player.getAbility()).toBe(enemy.getAbility()); + expect(player.getGender()).toBe(enemy.getGender()); + + expect(player.getStat(Stat.HP, false)).not.toBe(enemy.getStat(Stat.HP)); + for (const s of EFFECTIVE_STATS) { + expect(player.getStat(s, false)).toBe(enemy.getStat(s, false)); + } + + for (const s of BATTLE_STATS) { + expect(player.getStatStage(s)).toBe(enemy.getStatStage(s)); + } + + const playerMoveset = player.getMoveset(); + const enemyMoveset = player.getMoveset(); + + for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) { + // TODO: Checks for 5 PP should be done here when that gets addressed + expect(playerMoveset[i]?.moveId).toBe(enemyMoveset[i]?.moveId); + } + + const playerTypes = player.getTypes(); + const enemyTypes = enemy.getTypes(); + + for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) { + expect(playerTypes[i]).toBe(enemyTypes[i]); + } + }, 20000); + + it("should copy in-battle overridden stats", async () => { + game.override.enemyMoveset(new Array(4).fill(Moves.POWER_SPLIT)); + + await game.startBattle([ + Species.DITTO + ]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + const avgAtk = Math.floor((player.getStat(Stat.ATK, false) + enemy.getStat(Stat.ATK, false)) / 2); + const avgSpAtk = Math.floor((player.getStat(Stat.SPATK, false) + enemy.getStat(Stat.SPATK, false)) / 2); + + game.move.select(Moves.TRANSFORM); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStat(Stat.ATK, false)).toBe(avgAtk); + expect(enemy.getStat(Stat.ATK, false)).toBe(avgAtk); + + expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); + expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk); + }); +}); diff --git a/src/test/moves/wide_guard.test.ts b/src/test/moves/wide_guard.test.ts index 616972de01b..6feeff815b5 100644 --- a/src/test/moves/wide_guard.test.ts +++ b/src/test/moves/wide_guard.test.ts @@ -1,12 +1,12 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { BerryPhase } from "#app/phases/berry-phase"; -import { CommandPhase } from "#app/phases/command-phase"; -import { Abilities } from "#enums/abilities"; -import { Moves } from "#enums/moves"; -import { Species } from "#enums/species"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; import GameManager from "../utils/gameManager"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Stat } from "#enums/stat"; +import { BerryPhase } from "#app/phases/berry-phase"; +import { CommandPhase } from "#app/phases/command-phase"; const TIMEOUT = 20 * 1000; @@ -75,7 +75,7 @@ describe("Moves - Wide Guard", () => { await game.phaseInterceptor.to(BerryPhase, false); - leadPokemon.forEach(p => expect(p.summonData.battleStats[BattleStat.ATK]).toBe(0)); + leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0)); }, TIMEOUT ); diff --git a/src/test/utils/helpers/overridesHelper.ts b/src/test/utils/helpers/overridesHelper.ts index 6451155cf17..cc5f9018325 100644 --- a/src/test/utils/helpers/overridesHelper.ts +++ b/src/test/utils/helpers/overridesHelper.ts @@ -281,6 +281,17 @@ export class OverridesHelper extends GameManagerHelper { return this; } + /** + * Override the items rolled at the end of a battle + * @param items the items to be rolled + * @returns this + */ + itemRewards(items: ModifierOverride[]) { + vi.spyOn(Overrides, "ITEM_REWARD_OVERRIDE", "get").mockReturnValue(items); + this.log("Item rewards set to:", items); + return this; + } + /** * Override the enemy (Pokemon) to have the given amount of health segments * @param healthSegments the number of segments to give diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts index de65405abff..389ae36635a 100644 --- a/src/test/utils/phaseInterceptor.ts +++ b/src/test/utils/phaseInterceptor.ts @@ -1,4 +1,5 @@ import { Phase } from "#app/phase"; +import ErrorInterceptor from "#app/test/utils/errorInterceptor"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { BerryPhase } from "#app/phases/berry-phase"; import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; @@ -17,7 +18,6 @@ import { MoveEndPhase } from "#app/phases/move-end-phase"; import { MovePhase } from "#app/phases/move-phase"; import { NewBattlePhase } from "#app/phases/new-battle-phase"; import { NextEncounterPhase } from "#app/phases/next-encounter-phase"; -import { PartyHealPhase } from "#app/phases/party-heal-phase"; import { PostSummonPhase } from "#app/phases/post-summon-phase"; import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase"; import { SelectGenderPhase } from "#app/phases/select-gender-phase"; @@ -26,7 +26,7 @@ import { SelectStarterPhase } from "#app/phases/select-starter-phase"; import { SelectTargetPhase } from "#app/phases/select-target-phase"; import { ShinySparklePhase } from "#app/phases/shiny-sparkle-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; -import { StatChangePhase } from "#app/phases/stat-change-phase"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { SummonPhase } from "#app/phases/summon-phase"; import { SwitchPhase } from "#app/phases/switch-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; @@ -37,7 +37,7 @@ import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { TurnStartPhase } from "#app/phases/turn-start-phase"; import { UnavailablePhase } from "#app/phases/unavailable-phase"; import { VictoryPhase } from "#app/phases/victory-phase"; -import ErrorInterceptor from "#app/test/utils/errorInterceptor"; +import { PartyHealPhase } from "#app/phases/party-heal-phase"; import UI, { Mode } from "#app/ui/ui"; export default class PhaseInterceptor { @@ -86,7 +86,7 @@ export default class PhaseInterceptor { [NewBattlePhase, this.startPhase], [VictoryPhase, this.startPhase], [MoveEndPhase, this.startPhase], - [StatChangePhase, this.startPhase], + [StatStageChangePhase, this.startPhase], [ShinySparklePhase, this.startPhase], [SelectTargetPhase, this.startPhase], [UnavailablePhase, this.startPhase], diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index 11b807e8ab7..05c634609f8 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -7,7 +7,7 @@ import { StatusEffect } from "../data/status-effect"; import BattleScene from "../battle-scene"; import { Type, getTypeRgb } from "../data/type"; import { getVariantTint } from "#app/data/variant"; -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import BattleFlyout from "./battle-flyout"; import { WindowVariant, addWindow } from "./ui-theme"; import i18next from "i18next"; @@ -30,7 +30,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { private lastLevelExp: integer; private lastLevel: integer; private lastLevelCapped: boolean; - private lastBattleStats: string; + private lastStats: string; private box: Phaser.GameObjects.Sprite; private nameText: Phaser.GameObjects.Text; @@ -68,9 +68,9 @@ export default class BattleInfo extends Phaser.GameObjects.Container { public flyoutMenu?: BattleFlyout; - private battleStatOrder: BattleStat[]; - private battleStatOrderPlayer = [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.ACC, BattleStat.EVA, BattleStat.SPD]; - private battleStatOrderEnemy = [BattleStat.HP, BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.ACC, BattleStat.EVA, BattleStat.SPD]; + private statOrder: Stat[]; + private readonly statOrderPlayer = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA, Stat.SPD ]; + private readonly statOrderEnemy = [ Stat.HP, Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA, Stat.SPD ]; constructor(scene: Phaser.Scene, x: number, y: number, player: boolean) { super(scene, x, y); @@ -229,9 +229,9 @@ export default class BattleInfo extends Phaser.GameObjects.Container { const startingX = this.player ? -this.statsBox.width + 8 : -this.statsBox.width + 5; const paddingX = this.player ? 4 : 2; const statOverflow = this.player ? 1 : 0; - this.battleStatOrder = this.player ? this.battleStatOrderPlayer : this.battleStatOrderEnemy; // this tells us whether or not to use the player or enemy battle stat order + this.statOrder = this.player ? this.statOrderPlayer : this.statOrderEnemy; // this tells us whether or not to use the player or enemy battle stat order - this.battleStatOrder.map((s, i) => { + this.statOrder.map((s, i) => { // we do a check for i > statOverflow to see when the stat labels go onto the next column // For enemies, we have HP (i=0) by itself then a new column, so we check for i > 0 // For players, we don't have HP, so we start with i = 0 and i = 1 for our first column, and so need to check for i > 1 @@ -239,25 +239,25 @@ export default class BattleInfo extends Phaser.GameObjects.Container { const baseY = -this.statsBox.height / 2 + 4; // this is the baseline for the y-axis let statY: number; // this will be the y-axis placement for the labels - if (this.battleStatOrder[i] === BattleStat.SPD || this.battleStatOrder[i] === BattleStat.HP) { + if (this.statOrder[i] === Stat.SPD || this.statOrder[i] === Stat.HP) { statY = baseY + 5; } else { statY = baseY + (!!(i % 2) === this.player ? 10 : 0); // we compare i % 2 against this.player to tell us where to place the label; because this.battleStatOrder for enemies has HP, this.battleStatOrder[1]=ATK, but for players this.battleStatOrder[0]=ATK, so this comparing i % 2 to this.player fixes this issue for us } - const statLabel = this.scene.add.sprite(statX, statY, "pbinfo_stat", BattleStat[s]); + const statLabel = this.scene.add.sprite(statX, statY, "pbinfo_stat", Stat[s]); statLabel.setName("icon_stat_label_" + i.toString()); statLabel.setOrigin(0, 0); statLabels.push(statLabel); this.statValuesContainer.add(statLabel); - const statNumber = this.scene.add.sprite(statX + statLabel.width, statY, "pbinfo_stat_numbers", this.battleStatOrder[i] !== BattleStat.HP ? "3" : "empty"); + const statNumber = this.scene.add.sprite(statX + statLabel.width, statY, "pbinfo_stat_numbers", this.statOrder[i] !== Stat.HP ? "3" : "empty"); statNumber.setName("icon_stat_number_" + i.toString()); statNumber.setOrigin(0, 0); this.statNumbers.push(statNumber); this.statValuesContainer.add(statNumber); - if (this.battleStatOrder[i] === BattleStat.HP) { + if (this.statOrder[i] === Stat.HP) { statLabel.setVisible(false); statNumber.setVisible(false); } @@ -433,10 +433,10 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.statValuesContainer.setPosition(8, 7); } - const battleStats = this.battleStatOrder.map(() => 0); + const stats = this.statOrder.map(() => 0); - this.lastBattleStats = battleStats.join(""); - this.updateBattleStats(battleStats); + this.lastStats = stats.join(""); + this.updateStats(stats); } getTextureName(): string { @@ -650,14 +650,12 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.lastLevel = pokemon.level; } - const battleStats = pokemon.summonData - ? pokemon.summonData.battleStats - : this.battleStatOrder.map(() => 0); - const battleStatsStr = battleStats.join(""); + const stats = pokemon.getStatStages(); + const statsStr = stats.join(""); - if (this.lastBattleStats !== battleStatsStr) { - this.updateBattleStats(battleStats); - this.lastBattleStats = battleStatsStr; + if (this.lastStats !== statsStr) { + this.updateStats(stats); + this.lastStats = statsStr; } this.shinyIcon.setVisible(pokemon.isShiny()); @@ -769,10 +767,10 @@ export default class BattleInfo extends Phaser.GameObjects.Container { } } - updateBattleStats(battleStats: integer[]): void { - this.battleStatOrder.map((s, i) => { - if (s !== BattleStat.HP) { - this.statNumbers[i].setFrame(battleStats[s].toString()); + updateStats(stats: integer[]): void { + this.statOrder.map((s, i) => { + if (s !== Stat.HP) { + this.statNumbers[i].setFrame(stats[s - 1].toString()); } }); } diff --git a/src/ui/battle-message-ui-handler.ts b/src/ui/battle-message-ui-handler.ts index 86f8d9e01a8..4c2b798558a 100644 --- a/src/ui/battle-message-ui-handler.ts +++ b/src/ui/battle-message-ui-handler.ts @@ -1,13 +1,12 @@ import BattleScene from "../battle-scene"; import { addBBCodeTextObject, addTextObject, getTextColor, TextStyle } from "./text"; import { Mode } from "./ui"; -import * as Utils from "../utils"; import MessageUiHandler from "./message-ui-handler"; -import { getStatName, Stat } from "../data/pokemon-stat"; import { addWindow } from "./ui-theme"; import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; import {Button} from "#enums/buttons"; import i18next from "i18next"; +import { Stat, PERMANENT_STATS, getStatKey } from "#app/enums/stat"; export default class BattleMessageUiHandler extends MessageUiHandler { private levelUpStatsContainer: Phaser.GameObjects.Container; @@ -100,9 +99,8 @@ export default class BattleMessageUiHandler extends MessageUiHandler { const levelUpStatsLabelsContent = addTextObject(this.scene, (this.scene.game.canvas.width / 6) - 73, -94, "", TextStyle.WINDOW, { maxLines: 6 }); let levelUpStatsLabelText = ""; - const stats = Utils.getEnumValues(Stat); - for (const s of stats) { - levelUpStatsLabelText += `${getStatName(s)}\n`; + for (const s of PERMANENT_STATS) { + levelUpStatsLabelText += `${i18next.t(getStatKey(s))}\n`; } levelUpStatsLabelsContent.text = levelUpStatsLabelText; levelUpStatsLabelsContent.x -= levelUpStatsLabelsContent.displayWidth; @@ -176,8 +174,7 @@ export default class BattleMessageUiHandler extends MessageUiHandler { } const newStats = (this.scene as BattleScene).getParty()[partyMemberIndex].stats; let levelUpStatsValuesText = ""; - const stats = Utils.getEnumValues(Stat); - for (const s of stats) { + for (const s of PERMANENT_STATS) { levelUpStatsValuesText += `${showTotals ? newStats[s] : newStats[s] - prevStats[s]}\n`; } this.levelUpStatsValuesContent.text = levelUpStatsValuesText; @@ -199,10 +196,9 @@ export default class BattleMessageUiHandler extends MessageUiHandler { return new Promise(resolve => { this.scene.executeWithSeedOffset(() => { let levelUpStatsValuesText = ""; - const stats = Utils.getEnumValues(Stat); const shownStats = this.getTopIvs(ivs, shownIvsCount); - for (const s of stats) { - levelUpStatsValuesText += `${shownStats.indexOf(s) > -1 ? this.getIvDescriptor(ivs[s], s, pokemonId) : "???"}\n`; + for (const s of PERMANENT_STATS) { + levelUpStatsValuesText += `${shownStats.includes(s) ? this.getIvDescriptor(ivs[s], s, pokemonId) : "???"}\n`; } this.levelUpStatsValuesContent.text = levelUpStatsValuesText; this.levelUpStatsIncrContent.setVisible(false); @@ -217,26 +213,17 @@ export default class BattleMessageUiHandler extends MessageUiHandler { } getTopIvs(ivs: integer[], shownIvsCount: integer): Stat[] { - const stats = Utils.getEnumValues(Stat); let shownStats: Stat[] = []; if (shownIvsCount < 6) { - const statsPool = stats.slice(0); + let highestIv = -1; for (let i = 0; i < shownIvsCount; i++) { - let shownStat: Stat | null = null; - let highestIv = -1; - statsPool.map(s => { - if (ivs[s] > highestIv) { - shownStat = s as Stat; - highestIv = ivs[s]; - } - }); - if (shownStat !== null && shownStat !== undefined) { - shownStats.push(shownStat); - statsPool.splice(statsPool.indexOf(shownStat), 1); + if (ivs[i] > highestIv) { + shownStats.push(PERMANENT_STATS[i]); + highestIv = ivs[i]; } } } else { - shownStats = stats; + shownStats = PERMANENT_STATS.slice(); } return shownStats; } diff --git a/src/ui/stats-container.ts b/src/ui/stats-container.ts index 2bd7099a2c5..c6e0ea3a71c 100644 --- a/src/ui/stats-container.ts +++ b/src/ui/stats-container.ts @@ -1,7 +1,8 @@ import BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText"; import BattleScene from "../battle-scene"; -import { Stat, getStatName } from "../data/pokemon-stat"; import { TextStyle, addBBCodeTextObject, addTextObject, getTextColor } from "./text"; +import { PERMANENT_STATS, getStatKey } from "#app/enums/stat"; +import i18next from "i18next"; const ivChartSize = 24; const ivChartStatCoordMultipliers = [[0, -1], [0.825, -0.5], [0.825, 0.5], [-0.825, -0.5], [-0.825, 0.5], [0, 1]]; @@ -53,16 +54,16 @@ export class StatsContainer extends Phaser.GameObjects.Container { this.ivStatValueTexts = []; - new Array(6).fill(null).map((_, i: integer) => { - const statLabel = addTextObject(this.scene, ivChartBg.x + (ivChartSize) * ivChartStatCoordMultipliers[i][0] * 1.325, ivChartBg.y + (ivChartSize) * ivChartStatCoordMultipliers[i][1] * 1.325 - 4 + ivLabelOffset[i], getStatName(i as Stat), TextStyle.TOOLTIP_CONTENT); + for (const s of PERMANENT_STATS) { + const statLabel = addTextObject(this.scene, ivChartBg.x + (ivChartSize) * ivChartStatCoordMultipliers[s][0] * 1.325, ivChartBg.y + (ivChartSize) * ivChartStatCoordMultipliers[s][1] * 1.325 - 4 + ivLabelOffset[s], i18next.t(getStatKey(s)), TextStyle.TOOLTIP_CONTENT); statLabel.setOrigin(0.5); - this.ivStatValueTexts[i] = addBBCodeTextObject(this.scene, statLabel.x, statLabel.y + 8, "0", TextStyle.TOOLTIP_CONTENT); - this.ivStatValueTexts[i].setOrigin(0.5); + this.ivStatValueTexts[s] = addBBCodeTextObject(this.scene, statLabel.x, statLabel.y + 8, "0", TextStyle.TOOLTIP_CONTENT); + this.ivStatValueTexts[s].setOrigin(0.5); this.add(statLabel); - this.add(this.ivStatValueTexts[i]); - }); + this.add(this.ivStatValueTexts[s]); + } } updateIvs(ivs: integer[], originalIvs?: integer[]): void { diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index ea7b798f2bf..8ae72f08edd 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -11,7 +11,6 @@ import Move, { MoveCategory } from "../data/move"; import { getPokeballAtlasKey } from "../data/pokeball"; import { getGenderColor, getGenderSymbol } from "../data/gender"; import { getLevelRelExp, getLevelTotalExp } from "../data/exp"; -import { Stat, getStatName } from "../data/pokemon-stat"; import { PokemonHeldItemModifier } from "../modifier/modifier"; import { StatusEffect } from "../data/status-effect"; import { getBiomeName } from "../data/biomes"; @@ -19,10 +18,11 @@ import { Nature, getNatureName, getNatureStatMultiplier } from "../data/nature"; import { loggedInUser } from "../account"; import { Variant, getVariantTint } from "#app/data/variant"; import {Button} from "#enums/buttons"; -import { Ability } from "../data/ability.js"; +import { Ability } from "../data/ability"; import i18next from "i18next"; import {modifierSortFunc} from "../modifier/modifier"; import { PlayerGender } from "#enums/player-gender"; +import { Stat, PERMANENT_STATS, getStatKey } from "#app/enums/stat"; enum Page { PROFILE, @@ -836,10 +836,8 @@ export default class SummaryUiHandler extends UiHandler { const statsContainer = this.scene.add.container(0, -pageBg.height); pageContainer.add(statsContainer); - const stats = Utils.getEnumValues(Stat) as Stat[]; - - stats.forEach((stat, s) => { - const statName = getStatName(stat); + PERMANENT_STATS.forEach((stat, s) => { + const statName = i18next.t(getStatKey(stat)); const rowIndex = s % 3; const colIndex = Math.floor(s / 3); @@ -850,7 +848,7 @@ export default class SummaryUiHandler extends UiHandler { statsContainer.add(statLabel); const statValueText = stat !== Stat.HP - ? Utils.formatStat(this.pokemon?.stats[s]!) // TODO: is this bang correct? + ? Utils.formatStat(this.pokemon?.getStat(stat)!) // TODO: is this bang correct? : `${Utils.formatStat(this.pokemon?.hp!, true)}/${Utils.formatStat(this.pokemon?.getMaxHp()!, true)}`; // TODO: are those bangs correct? const statValue = addTextObject(this.scene, 120 + 88 * colIndex, 56 + 16 * rowIndex, statValueText, TextStyle.WINDOW_ALT);