From db9925ec0fbe07768c07e138ae1bc5712e817559 Mon Sep 17 00:00:00 2001 From: xsn34kzx Date: Fri, 16 Aug 2024 21:09:11 -0400 Subject: [PATCH] Remove `BattleStat`, Use "Stat Stages" & New Names --- src/data/ability.ts | 504 +++++++++---------- src/data/arena-tag.ts | 6 +- src/data/battle-stat.ts | 72 --- src/data/battler-tags.ts | 62 ++- src/data/berry.ts | 19 +- src/data/move.ts | 576 +++++++++++----------- src/data/nature.ts | 7 +- src/data/pokemon-stat.ts | 29 -- src/enums/stat.ts | 22 + src/field/pokemon.ts | 87 ++-- src/locales/ca_ES/achv.ts | 4 +- src/locales/de/achv.ts | 4 +- src/locales/de/pokemon-info.ts | 1 - src/locales/en/achv.ts | 4 +- src/locales/en/pokemon-info.ts | 3 +- src/locales/es/achv.ts | 2 +- src/locales/fr/achv.ts | 4 +- src/locales/it/achv.ts | 2 +- src/locales/ja/achv.ts | 2 +- src/locales/ko/achv.ts | 2 +- src/locales/pt_BR/achv.ts | 4 +- src/locales/zh_CN/achv.ts | 2 +- src/locales/zh_CN/pokemon-info.ts | 2 +- src/locales/zh_TW/achv.ts | 2 +- src/modifier/modifier-type.ts | 6 +- src/phases.ts | 61 ++- src/system/achv.ts | 15 +- src/test/abilities/costar.test.ts | 22 +- src/test/abilities/gulp_missile.test.ts | 10 +- src/test/abilities/hustle.test.ts | 8 +- src/test/abilities/intimidate.test.ts | 344 ++----------- src/test/abilities/intrepid_sword.test.ts | 15 +- src/test/abilities/moxie.test.ts | 55 ++- 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/volt_absorb.test.ts | 8 +- src/test/abilities/wind_rider.test.ts | 38 +- src/test/achievements/achievement.test.ts | 2 +- src/test/battle-stat.spec.ts | 6 +- src/test/battle/battle.test.ts | 4 +- src/test/battlerTags/octolock.test.ts | 8 +- src/test/battlerTags/stockpiling.test.ts | 54 +- src/test/items/eviolite.test.ts | 12 +- src/test/items/light_ball.test.ts | 12 +- src/test/items/metal_powder.test.ts | 12 +- src/test/items/quick_powder.test.ts | 12 +- src/test/items/thick_club.test.ts | 12 +- src/test/moves/baton_pass.test.ts | 21 +- src/test/moves/belly_drum.test.ts | 28 +- src/test/moves/clangorous_soul.test.ts | 102 ++-- src/test/moves/double_team.test.ts | 8 +- src/test/moves/dragon_rage.test.ts | 29 +- src/test/moves/fillet_away.test.ts | 38 +- src/test/moves/fissure.test.ts | 12 +- src/test/moves/flower_shield.test.ts | 42 +- src/test/moves/follow_me.test.ts | 2 +- src/test/moves/freezy_frost.test.ts | 96 ++-- src/test/moves/growth.test.ts | 41 +- src/test/moves/haze.test.ts | 38 +- src/test/moves/make_it_rain.test.ts | 20 +- src/test/moves/octolock.test.ts | 22 +- src/test/moves/spit_up.test.ts | 59 +-- src/test/moves/spotlight.test.ts | 2 +- src/test/moves/stockpile.test.ts | 36 +- src/test/moves/swallow.test.ts | 27 +- src/test/moves/tail_whip.test.ts | 29 +- src/test/moves/tailwind.test.ts | 16 +- src/test/moves/tidy_up.test.ts | 19 +- src/ui/battle-info.ts | 42 +- src/ui/battle-message-ui-handler.ts | 7 +- src/ui/stats-container.ts | 14 +- src/ui/summary-ui-handler.ts | 10 +- 73 files changed, 1312 insertions(+), 1670 deletions(-) delete mode 100644 src/data/battle-stat.ts delete mode 100644 src/data/pokemon-stat.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 950538af9f6..e6aadd50a11 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2,8 +2,7 @@ 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 { MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases"; +import { MovePhase, PokemonHealPhase, ShowAbilityPhase, StatStageChangePhase } from "../phases"; import { getPokemonNameWithAffix } from "../messages"; import { Weather, WeatherType } from "./weather"; import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags"; @@ -11,7 +10,6 @@ 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 } 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 } from "./pokemon-forms"; @@ -26,6 +24,7 @@ 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, BattleStat, getStatKey, BATTLE_STATS, EFFECTIVE_STATS, EffectiveStat } from "#app/enums/stat.js"; export class Ability implements Localizable { public id: Abilities; @@ -122,7 +121,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: integer) => boolean; export abstract class AbAttr { public showAbility: boolean; @@ -199,37 +198,35 @@ 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: integer, 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, args: any[]): boolean { - const statChangePhases: StatChangePhase[] = []; + const statStageChangePhases: StatStageChangePhase[] = []; 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); } } @@ -415,15 +412,15 @@ export class TypeImmunityHealAbAttr extends TypeImmunityAbAttr { } } -class TypeImmunityStatChangeAbAttr extends TypeImmunityAbAttr { +class TypeImmunityStatStageChangeAbAttr extends TypeImmunityAbAttr { private stat: BattleStat; - private levels: integer; + private stages: integer; - constructor(immuneType: Type, stat: BattleStat, levels: integer, condition?: AbAttrCondition) { + constructor(immuneType: Type, stat: BattleStat, stages: integer, condition?: AbAttrCondition) { super(immuneType, condition); this.stat = stat; - this.levels = levels; + this.stages = stages; } applyPreDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { @@ -433,7 +430,7 @@ class TypeImmunityStatChangeAbAttr extends TypeImmunityAbAttr { cancelled.value = true; const simulated = args.length > 1 && args[1]; 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)); } } @@ -529,7 +526,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); } @@ -597,8 +594,8 @@ export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr { } } -export class PostStatChangeAbAttr extends AbAttr { - applyPostStatChange(pokemon: Pokemon, statsChanged: BattleStat[], levelChanged: integer, selfTarget: boolean, args: any[]): boolean | Promise { +export class PostStatStageChangeAbAttr extends AbAttr { + applyPostStatStageChange(pokemon: Pokemon, statsChanged: BattleStat[], levelChanged: integer, selfTarget: boolean, args: any[]): boolean | Promise { return false; } } @@ -644,14 +641,14 @@ export class WonderSkinAbAttr extends PreDefendAbAttr { } } -export class MoveImmunityStatChangeAbAttr extends MoveImmunityAbAttr { +export class MoveImmunityStatStageChangeAbAttr extends MoveImmunityAbAttr { private stat: BattleStat; - private levels: integer; + private stages: integer; - constructor(immuneCondition: PreDefendAbAttrCondition, stat: BattleStat, levels: integer) { + constructor(immuneCondition: PreDefendAbAttrCondition, stat: BattleStat, stages: integer) { super(immuneCondition); this.stat = stat; - this.levels = levels; + this.stages = stages; } applyPreDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { @@ -659,7 +656,7 @@ export class MoveImmunityStatChangeAbAttr extends MoveImmunityAbAttr { if (ret) { const simulated = args.length > 1 && args[1]; 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)); } } @@ -693,19 +690,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: integer; 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: integer, 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; } @@ -715,11 +712,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; } @@ -727,20 +724,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: integer; private selfTarget: boolean; - constructor(condition: PokemonDefendCondition, hpGate: number, stats: BattleStat[], levels: integer, selfTarget: boolean = true) { + constructor(condition: PokemonDefendCondition, hpGate: number, stats: BattleStat[], stages: integer, selfTarget: boolean = true) { super(true); this.condition = condition; this.hpGate = hpGate; this.stats = stats; - this.levels = levels; + this.stages = stages; this.selfTarget = selfTarget; } @@ -750,7 +747,7 @@ export class PostDefendHpGatedStatChangeAbAttr extends PostDefendAbAttr { const damageReceived = lastAttackReceived?.damage || 0; if (this.condition(pokemon, attacker, move) && (pokemon.hp <= hpGateFlat && (pokemon.hp + damageReceived) > hpGateFlat)) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, (this.selfTarget ? pokemon : attacker).getBattlerIndex(), true, this.stats, this.levels)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, (this.selfTarget ? pokemon : attacker).getBattlerIndex(), true, this.stats, this.stages)); return true; } @@ -900,19 +897,19 @@ export class PostDefendContactApplyTagChanceAbAttr extends PostDefendAbAttr { } } -export class PostDefendCritStatChangeAbAttr extends PostDefendAbAttr { +export class PostDefendCritStatStageChangeAbAttr extends PostDefendAbAttr { private stat: BattleStat; - private levels: integer; + private stages: integer; - constructor(stat: BattleStat, levels: integer) { + constructor(stat: BattleStat, stages: integer) { super(); this.stat = stat; - this.levels = levels; + this.stages = stages; } applyPostDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - 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; } @@ -1085,22 +1082,22 @@ 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: integer; - constructor(condition: PokemonStatChangeCondition, statsToChange: BattleStat[], levels: integer) { + constructor(condition: PokemonStatStageChangeCondition, statsToChange: BattleStat[], stages: integer) { super(true); this.condition = condition; this.statsToChange = statsToChange; - this.levels = levels; + this.stages = stages; } - applyPostStatChange(pokemon: Pokemon, statsChanged: BattleStat[], levelsChanged: integer, selfTarget: boolean, args: any[]): boolean { - if (this.condition(pokemon, statsChanged, levelsChanged) && !selfTarget) { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, (pokemon).getBattlerIndex(), true, this.statsToChange, this.levels)); + applyPostStatStageChange(pokemon: Pokemon, statsChanged: BattleStat[], stagesChanged: integer, selfTarget: boolean, args: any[]): boolean { + if (this.condition(pokemon, statsChanged, stagesChanged) && !selfTarget) { + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, (pokemon).getBattlerIndex(), true, this.statsToChange, this.stages)); return true; } @@ -1180,13 +1177,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; @@ -1200,7 +1197,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 @@ -1210,12 +1207,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, stat: Stat, statValue: Utils.NumberHolder, checkedPokemon: Pokemon, hasApplied: Utils.BooleanHolder, args: any[]): boolean { + applyFieldStat(pokemon: Pokemon, passive: 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; @@ -1554,22 +1551,22 @@ export class AllyMoveCategoryPowerBoostAbAttr extends FieldMovePowerBoostAbAttr } } -export class BattleStatMultiplierAbAttr extends AbAttr { - private battleStat: BattleStat; +export class StatStageMultiplierAbAttr 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!; // TODO: is this bang correct? } - applyBattleStat(pokemon: Pokemon, passive: boolean, battleStat: BattleStat, statValue: Utils.NumberHolder, args: any[]): boolean | Promise { + applyStatStage(pokemon: Pokemon, _passive: 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; } @@ -1740,22 +1737,22 @@ export class PostVictoryAbAttr extends AbAttr { } } -class PostVictoryStatChangeAbAttr extends PostVictoryAbAttr { +class PostVictoryStatStageChangeAbAttr extends PostVictoryAbAttr { private stat: BattleStat | ((p: Pokemon) => BattleStat); - private levels: integer; + private stages: integer; - constructor(stat: BattleStat | ((p: Pokemon) => BattleStat), levels: integer) { + constructor(stat: BattleStat | ((p: Pokemon) => BattleStat), stages: integer) { super(); this.stat = stat; - this.levels = levels; + this.stages = stages; } applyPostVictory(pokemon: Pokemon, passive: boolean, args: any[]): boolean | Promise { const stat = typeof this.stat === "function" ? this.stat(pokemon) : this.stat; - 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; } @@ -1787,22 +1784,22 @@ 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: integer; - constructor(stat: BattleStat | ((p: Pokemon) => BattleStat), levels: integer) { + constructor(stat: BattleStat | ((p: Pokemon) => BattleStat), stages: integer) { super(); this.stat = stat; - this.levels = levels; + this.stages = stages; } applyPostKnockOut(pokemon: Pokemon, passive: boolean, knockedOut: Pokemon, args: any[]): boolean | Promise { const stat = typeof this.stat === "function" ? this.stat(pokemon) : this.stat; - 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; } @@ -1824,7 +1821,7 @@ export class CopyFaintedAllyAbilityAbAttr extends PostKnockOutAbAttr { } } -export class IgnoreOpponentStatChangesAbAttr extends AbAttr { +export class IgnoreOpponentStatStageChangesAbAttr extends AbAttr { constructor() { super(false); } @@ -1849,7 +1846,7 @@ export class IgnoreOpponentEvasionAbAttr extends AbAttr { * @param pokemon N/A * @param passive N/A * @param cancelled N/A - * @param args [0] {@linkcode Utils.IntegerHolder} of BattleStat.EVA + * @param args [0] {@linkcode Utils.IntegerHolder} of Stat.EVA * @returns if evasion level was successfully considered as 0 */ apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]) { @@ -1876,20 +1873,20 @@ export class IntimidateImmunityAbAttr extends AbAttr { } } -export class PostIntimidateStatChangeAbAttr extends AbAttr { +export class PostIntimidateStatStageChangeAbAttr extends AbAttr { private stats: BattleStat[]; - private levels: integer; + private stages: integer; private overwrites: boolean; - constructor(stats: BattleStat[], levels: integer, overwrites?: boolean) { + constructor(stats: BattleStat[], stages: integer, overwrites?: boolean) { super(true); this.stats = stats; - this.levels = levels; + this.stages = stages; this.overwrites = !!overwrites; } apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { - 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; } @@ -1983,19 +1980,17 @@ export class PostSummonAddBattlerTagAbAttr extends PostSummonAbAttr { } } -export class PostSummonStatChangeAbAttr extends PostSummonAbAttr { +export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr { private stats: BattleStat[]; - private levels: integer; + private stages: integer; private selfTarget: boolean; private intimidate: boolean; - constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean, intimidate?: boolean) { + constructor(stats: BattleStat[], stages: integer, 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; } @@ -2003,19 +1998,19 @@ export class PostSummonStatChangeAbAttr extends PostSummonAbAttr { applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean { 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); - applyAbAttrs(PostIntimidateStatChangeAbAttr, opponent, cancelled); + applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled); } if (!cancelled.value) { - const statChangePhase = new StatChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.levels); + const statChangePhase = new StatStageChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.stages); pokemon.scene.unshiftPhase(statChangePhase); } } @@ -2054,7 +2049,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(); } @@ -2062,8 +2057,8 @@ export class PostSummonClearAllyStatsAbAttr extends PostSummonAbAttr { applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean { const target = pokemon.getAlly(); if (target?.isActive(true)) { - 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) })); @@ -2091,7 +2086,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 @@ -2104,20 +2099,20 @@ 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 - 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; } @@ -2274,13 +2269,15 @@ 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; } - pokemon.summonData.battleStats = ally.summonData.battleStats; - pokemon.updateInfo(); + for (const s of BATTLE_STATS) { + pokemon.setStatStage(s, ally.getStatStage(s)); + } + pokemon.updateInfo(); return true; } @@ -2316,8 +2313,17 @@ 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(); @@ -2445,13 +2451,13 @@ export class PreSwitchOutFormChangeAbAttr extends PreSwitchOutAbAttr { } -export class PreStatChangeAbAttr extends AbAttr { - applyPreStatChange(pokemon: Pokemon | null, passive: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise { +export class PreStatStageChangeAbAttr extends AbAttr { + applyPreStatStageChange(pokemon: Pokemon | null, passive: 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 | null; constructor(protectedStat?: BattleStat) { @@ -2460,7 +2466,7 @@ export class ProtectStatAbAttr extends PreStatChangeAbAttr { this.protectedStat = protectedStat!; // TODO: is this bang correct? } - applyPreStatChange(pokemon: Pokemon, passive: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean { + applyPreStatStageChange(pokemon: Pokemon, passive: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean { if (this.protectedStat === undefined || stat === this.protectedStat) { cancelled.value = true; return true; @@ -2473,7 +2479,7 @@ export class ProtectStatAbAttr extends PreStatChangeAbAttr { 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") }); } } @@ -3221,38 +3227,37 @@ export class MoodyAbAttr extends PostTurnAbAttr { } applyPostTurn(pokemon: Pokemon, passive: 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 increaseStatArray = EFFECTIVE_STATS.filter(s => pokemon.getStatStage(s) < 6); + let decreaseStatArray = EFFECTIVE_STATS.filter(s => pokemon.getStatStage(s) > -6); if (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)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [increaseStat], 2)); } if (decreaseStatArray.length > 0) { - const decreaseStat = selectableStats[Utils.randInt(selectableStats.length)]; - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [decreaseStat], -1)); + const decreaseStat = EFFECTIVE_STATS[Utils.randInt(EFFECTIVE_STATS.length)]; + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [decreaseStat], -1)); } return true; } } -export class PostTurnStatChangeAbAttr extends PostTurnAbAttr { +export class PostTurnStatStageChangeAbAttr extends PostTurnAbAttr { private stats: BattleStat[]; - private levels: integer; + private stages: integer; - constructor(stats: BattleStat | BattleStat[], levels: integer) { + constructor(stats: BattleStat[], stages: integer) { super(true); this.stats = Array.isArray(stats) ? stats : [ stats ]; - this.levels = levels; + this.stages = stages; } applyPostTurn(pokemon: Pokemon, passive: boolean, args: any[]): boolean { - 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; } } @@ -3441,7 +3446,7 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { } } -export class StatChangeMultiplierAbAttr extends AbAttr { +export class StatStageChangeMultiplierAbAttr extends AbAttr { private multiplier: integer; constructor(multiplier: integer) { @@ -3457,9 +3462,9 @@ export class StatChangeMultiplierAbAttr extends AbAttr { } } -export class StatChangeCopyAbAttr extends AbAttr { +export class StatStageChangeCopyAbAttr extends AbAttr { apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise { - 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 integer), true, false, false)); return true; } } @@ -3793,21 +3798,21 @@ export class FlinchEffectAbAttr extends AbAttr { } } -export class FlinchStatChangeAbAttr extends FlinchEffectAbAttr { +export class FlinchStatStageChangeAbAttr extends FlinchEffectAbAttr { private stats: BattleStat[]; - private levels: integer; + private stages: integer; - constructor(stats: BattleStat | BattleStat[], levels: integer) { + constructor(stats: BattleStat[], stages: integer) { super(); this.stats = Array.isArray(stats) ? stats : [ stats ]; - this.levels = levels; + this.stages = stages; } apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { - 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; } } @@ -4004,9 +4009,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 @@ -4015,13 +4020,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; } @@ -4221,14 +4226,14 @@ export function applyPostMoveUsedAbAttrs(attrType: Constructor(attrType, pokemon, (attr, passive) => attr.applyPostMoveUsed(pokemon, move, source, targets, args), args); } -export function applyBattleStatMultiplierAbAttrs(attrType: Constructor, - pokemon: Pokemon, battleStat: BattleStat, statValue: Utils.NumberHolder, ...args: any[]): Promise { - return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyBattleStat(pokemon, passive, battleStat, statValue, args), args); +export function applyStatStageMultiplierAbAttrs(attrType: Constructor, + pokemon: Pokemon, stat: BattleStat, statValue: Utils.NumberHolder, ...args: any[]): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyStatStage(pokemon, passive, 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 @@ -4236,9 +4241,9 @@ export function applyBattleStatMultiplierAbAttrs(attrType: Constructor, +export function applyFieldStatMultiplierAbAttrs(attrType: Constructor, pokemon: Pokemon, stat: Stat, statValue: Utils.NumberHolder, checkedPokemon: Pokemon, hasApplied: Utils.BooleanHolder, ...args: any[]): Promise { - return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyFieldBattleStat(pokemon, passive, stat, statValue, checkedPokemon, hasApplied, args), args); + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyFieldStat(pokemon, passive, stat, statValue, checkedPokemon, hasApplied, args), args); } export function applyPreAttackAbAttrs(attrType: Constructor, @@ -4271,14 +4276,14 @@ export function applyPreSwitchOutAbAttrs(attrType: Constructor(attrType, pokemon, (attr, passive) => attr.applyPreSwitchOut(pokemon, passive, args), args, true); } -export function applyPreStatChangeAbAttrs(attrType: Constructor, +export function applyPreStatStageChangeAbAttrs(attrType: Constructor, pokemon: Pokemon | null, stat: BattleStat, cancelled: Utils.BooleanHolder, ...args: any[]): Promise { - return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPreStatChange(pokemon, passive, stat, cancelled, args), args); + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPreStatStageChange(pokemon, passive, stat, cancelled, args), args); } -export function applyPostStatChangeAbAttrs(attrType: Constructor, - pokemon: Pokemon, stats: BattleStat[], levels: integer, selfTarget: boolean, ...args: any[]): Promise { - return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostStatChange(pokemon, stats, levels, selfTarget, args), args); +export function applyPostStatStageChangeAbAttrs(attrType: Constructor, + pokemon: Pokemon, stats: BattleStat[], stages: integer, selfTarget: boolean, ...args: any[]): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostStatStageChange(pokemon, stats, stages, selfTarget, args), args); } export function applyPreSetStatusAbAttrs(attrType: Constructor, @@ -4358,7 +4363,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(), @@ -4373,7 +4378,7 @@ export function initAbilities() { .attr(StatusEffectImmunityAbAttr, StatusEffect.PARALYSIS) .ignorable(), new Ability(Abilities.SAND_VEIL, 3) - .attr(BattleStatMultiplierAbAttr, BattleStat.EVA, 1.2) + .attr(StatStageMultiplierAbAttr, Stat.EVA, 1.2) .attr(BlockWeatherDamageAttr, WeatherType.SANDSTORM) .condition(getWeatherCondition(WeatherType.SANDSTORM)) .ignorable(), @@ -4396,7 +4401,7 @@ export function initAbilities() { .attr(SuppressWeatherEffectAbAttr, true) .attr(PostSummonUnnamedMessageAbAttr, "The effects of the weather disappeared."), new Ability(Abilities.COMPOUND_EYES, 3) - .attr(BattleStatMultiplierAbAttr, BattleStat.ACC, 1.3), + .attr(StatStageMultiplierAbAttr, Stat.ACC, 1.3), new Ability(Abilities.INSOMNIA, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP) .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY) @@ -4421,7 +4426,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)) { @@ -4452,26 +4457,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(StatStageMultiplierAbAttr, Stat.SPD, 2) .condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)), new Ability(Abilities.CHLOROPHYLL, 3) - .attr(BattleStatMultiplierAbAttr, BattleStat.SPD, 2) + .attr(StatStageMultiplierAbAttr, 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(StatStageMultiplierAbAttr, Stat.ATK, 2), new Ability(Abilities.POISON_POINT, 3) .attr(PostDefendContactApplyStatusEffectAbAttr, 30, StatusEffect.POISON) .bypassFaint(), @@ -4516,25 +4521,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(StatStageMultiplierAbAttr, Stat.ATK, 1.5) + .attr(StatStageMultiplierAbAttr, 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)), StatStageMultiplierAbAttr, 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)), StatStageMultiplierAbAttr, Stat.SPATK, 1.5) .ignorable(), new Ability(Abilities.FORECAST, 3) .attr(UncopiableAbilityAbAttr) @@ -4548,9 +4553,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), StatStageMultiplierAbAttr, 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), StatStageMultiplierAbAttr, Stat.DEF, 1.5) .ignorable(), new Ability(Abilities.LIQUID_OOZE, 3) .attr(ReverseDrainAbAttr), @@ -4583,7 +4588,7 @@ export function initAbilities() { .attr(ProtectStatAbAttr) .ignorable(), new Ability(Abilities.PURE_POWER, 3) - .attr(BattleStatMultiplierAbAttr, BattleStat.ATK, 2), + .attr(StatStageMultiplierAbAttr, Stat.ATK, 2), new Ability(Abilities.SHELL_ARMOR, 3) .attr(BlockCritAbAttr) .ignorable(), @@ -4591,32 +4596,32 @@ export function initAbilities() { .attr(SuppressWeatherEffectAbAttr, true) .attr(PostSummonUnnamedMessageAbAttr, "The effects of the weather disappeared."), new Ability(Abilities.TANGLED_FEET, 4) - .conditionalAttr(pokemon => !!pokemon.getTag(BattlerTagType.CONFUSED), BattleStatMultiplierAbAttr, BattleStat.EVA, 2) + .conditionalAttr(pokemon => !!pokemon.getTag(BattlerTagType.CONFUSED), StatStageMultiplierAbAttr, 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(StatStageMultiplierAbAttr, 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) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 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) @@ -4641,11 +4646,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(StatStageMultiplierAbAttr, 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, StatStageMultiplierAbAttr, Stat.SPD, 2) + .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(Abilities.COMATOSE), StatStageMultiplierAbAttr, Stat.SPD, 1.5), new Ability(Abilities.NORMALIZE, 4) .attr(MoveTypeChangeAttr, 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); @@ -4685,7 +4690,7 @@ export function initAbilities() { new Ability(Abilities.FOREWARN, 4) .attr(ForewarnAbAttr), new Ability(Abilities.UNAWARE, 4) - .attr(IgnoreOpponentStatChangesAbAttr) + .attr(IgnoreOpponentStatStageChangesAbAttr) .ignorable(), new Ability(Abilities.TINTED_LENS, 4) //@ts-ignore @@ -4700,7 +4705,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) @@ -4724,8 +4729,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), StatStageMultiplierAbAttr, Stat.ATK, 1.5) + .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), StatStageMultiplierAbAttr, Stat.SPDEF, 1.5) .attr(UncopiableAbilityAbAttr) .attr(NoFusionAbilityAbAttr) .ignorable() @@ -4740,15 +4745,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(StatStageMultiplierAbAttr, Stat.ATK, 0.5) + .attr(StatStageMultiplierAbAttr, Stat.SPATK, 0.5) .condition((pokemon) => pokemon.getHpRatio() <= 0.5), new Ability(Abilities.CURSED_BODY, 5) .attr(PostDefendMoveDisableAbAttr, 30) @@ -4759,8 +4764,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(), @@ -4796,10 +4801,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(StatStageMultiplierAbAttr, Stat.SPD, 2) .attr(BlockWeatherDamageAttr, WeatherType.SANDSTORM) .condition(getWeatherCondition(WeatherType.SANDSTORM)), new Ability(Abilities.WONDER_SKIN, 5) @@ -4821,18 +4826,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(IncrementMovePriorityAbAttr, (pokemon, move: Move) => move.category === MoveCategory.STATUS), @@ -4855,7 +4860,7 @@ export function initAbilities() { .attr(NoFusionAbilityAbAttr) .bypassFaint(), new Ability(Abilities.VICTORY_STAR, 5) - .attr(BattleStatMultiplierAbAttr, BattleStat.ACC, 1.1) + .attr(StatStageMultiplierAbAttr, Stat.ACC, 1.1) .partial(), new Ability(Abilities.TURBOBLAZE, 5) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTurboblaze", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) @@ -4884,7 +4889,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) @@ -4904,7 +4909,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), StatStageMultiplierAbAttr, Stat.DEF, 1.5) .ignorable(), new Ability(Abilities.SYMBIOSIS, 6) .unimplemented(), @@ -4913,7 +4918,7 @@ export function initAbilities() { new Ability(Abilities.PIXILATE, 6) .attr(MoveTypeChangeAttr, Type.FAIRY, 1.2, (user, target, move) => move.type === Type.NORMAL), 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(MoveTypeChangeAttr, Type.FLYING, 1.2, (user, target, move) => move.type === Type.NORMAL), new Ability(Abilities.PARENTAL_BOND, 6) @@ -4947,7 +4952,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(), @@ -4955,7 +4960,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) @@ -4979,10 +4984,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(StatStageMultiplierAbAttr, Stat.SPD, 2) .condition(getWeatherCondition(WeatherType.HAIL, WeatherType.SNOW)), new Ability(Abilities.LONG_REACH, 7) .attr(IgnoreContactAbAttr), @@ -4993,7 +4998,7 @@ export function initAbilities() { new Ability(Abilities.GALVANIZE, 7) .attr(MoveTypeChangeAttr, Type.ELECTRIC, 1.2, (user, target, move) => move.type === Type.NORMAL), new Ability(Abilities.SURGE_SURFER, 7) - .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), BattleStatMultiplierAbAttr, BattleStat.SPD, 2), + .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), StatStageMultiplierAbAttr, Stat.SPD, 2), new Ability(Abilities.SCHOOLING, 7) .attr(PostBattleInitFormChangeAbAttr, () => 0) .attr(PostSummonFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1) @@ -5063,9 +5068,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), @@ -5073,18 +5078,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) @@ -5113,10 +5117,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), @@ -5125,7 +5129,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), @@ -5142,7 +5146,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) @@ -5210,26 +5214,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), @@ -5239,26 +5243,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) @@ -5299,31 +5303,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), StatStageMultiplierAbAttr, 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), StatStageMultiplierAbAttr, 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) @@ -5348,11 +5352,11 @@ export function initAbilities() { .partial(), new Ability(Abilities.MINDS_EYE, 9) .attr(IgnoreTypeImmunityAbAttr, Type.GHOST, [Type.NORMAL, Type.FIGHTING]) - .attr(ProtectStatAbAttr, BattleStat.ACC) + .attr(ProtectStatAbAttr, Stat.ACC) .attr(IgnoreOpponentEvasionAbAttr) .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) @@ -5360,25 +5364,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 7c67271b0dc..ed30178e7c9 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -8,7 +8,7 @@ import { MoveEffectPhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } import { StatusEffect } from "./status-effect"; import { BattlerIndex } from "../battle"; import { BlockNonDirectDamageAbAttr, 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"; @@ -726,7 +726,7 @@ class StickyWebTag extends ArenaTrapTag { 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)); + pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.SPD ], statLevels.value)); } } @@ -814,7 +814,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 StatChangePhase(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 1dd278701dd..00000000000 --- a/src/data/battle-stat.ts +++ /dev/null @@ -1,72 +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 "???"; - } -} - -// TODO: BattleStat -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 16dd4914ed4..6215557f3e0 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1,8 +1,7 @@ import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "./battle-anims"; -import { CommonAnimPhase, MoveEffectPhase, MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangeCallback, StatChangePhase } from "../phases"; +import { CommonAnimPhase, MoveEffectPhase, MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangeCallback, StatStageChangePhase } from "../phases"; 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"; @@ -10,7 +9,6 @@ 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"; @@ -18,6 +16,7 @@ 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 { Stat, BattleStat, EFFECTIVE_STATS, EffectiveStat, getStatKey } from "#app/enums/stat"; export enum BattlerTagLapseType { FAINT, @@ -296,8 +295,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 = Math.ceil(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedInt(15, 85) / 100)); pokemon.scene.queueMessage(i18next.t("battle:battlerTagsConfusedLapseHurtItself")); pokemon.damageAndUpdate(damage); @@ -701,7 +700,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(), true, [BattleStat.DEF, BattleStat.SPDEF], -1)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.DEF, Stat.SPDEF ], -1)); return true; } @@ -1027,7 +1026,7 @@ export class ContactDamageProtectedTag extends ProtectedTag { } } -export class ContactStatChangeProtectedTag extends ProtectedTag { +export class ContactStatStageChangeProtectedTag extends ProtectedTag { private stat: BattleStat; private levels: number; @@ -1044,7 +1043,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; } @@ -1055,7 +1054,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)); } } @@ -1282,11 +1281,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; @@ -1304,7 +1302,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag { break; } - pokemon.scene.queueMessage(i18next.t("battle:battlerTagsHighestStatBoostOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), statName: getStatName(highestStat) }), null, false, null, true); + pokemon.scene.queueMessage(i18next.t("battle:battlerTagsHighestStatBoostOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), statName: getStatKey(highestStat) }), null, false, null, true); } onRemove(pokemon: Pokemon): void { @@ -1622,9 +1620,9 @@ export class IceFaceTag extends BattlerTag { */ 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) { @@ -1632,15 +1630,15 @@ export class StockpilingTag extends BattlerTag { } private onStatsChanged: StatChangeCallback = (_, statsChanged, statChanges) => { - const defChange = statChanges[statsChanged.indexOf(BattleStat.DEF)] ?? 0; - const spDefChange = statChanges[statsChanged.indexOf(BattleStat.SPDEF)] ?? 0; + 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]++; } }; @@ -1648,8 +1646,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, }; } @@ -1669,9 +1667,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.onStatsChanged )); } } @@ -1685,15 +1683,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)); } } } @@ -1831,11 +1829,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 30b89848452..c0778a86d8e 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -1,13 +1,13 @@ import { PokemonHealPhase, StatChangePhase } from "../phases"; 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 { BattleStat, Stat } from "#app/enums/stat"; export function getBerryName(berryType: BerryType): string { return i18next.t(`berry:${BerryType[berryType]}.name`); @@ -34,9 +34,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, 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) => { @@ -94,10 +95,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, 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, statStages); + pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], statStages.value)); }; case BerryType.LANSAT: return (pokemon: Pokemon) => { @@ -111,9 +113,10 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { if (pokemon.battleData) { pokemon.battleData.berriesEaten.push(berryType); } + const randStat = Utils.randSeedInt(Stat.EVA, Stat.ATK); const statLevels = new Utils.NumberHolder(2); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, statLevels); - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ BattleStat.RAND ], statLevels.value)); + pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ randStat ], statLevels.value)); // TODO: BattleStats }; case BerryType.LEPPA: return (pokemon: Pokemon) => { diff --git a/src/data/move.ts b/src/data/move.ts index 26652d182ca..26913b81010 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1,6 +1,5 @@ import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims"; -import { BattleEndPhase, MoveEndPhase, MovePhase, NewBattlePhase, PartyStatusCurePhase, PokemonHealPhase, StatChangePhase, SwitchPhase, SwitchSummonPhase } from "../phases"; -import { BattleStat, getBattleStatName } from "./battle-stat"; +import { BattleEndPhase, MoveEndPhase, MovePhase, NewBattlePhase, PartyStatusCurePhase, PokemonHealPhase, StatStageChangePhase, SwitchPhase, SwitchSummonPhase } from "../phases"; import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, StockpilingTag, TypeBoostTag } from "./battler-tags"; import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon"; @@ -14,7 +13,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"; @@ -28,6 +26,7 @@ import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { MoveUsedEvent } from "#app/events/battle-scene.js"; +import { Stat, BATTLE_STATS, BattleStat, EFFECTIVE_STATS, EffectiveStat, getStatKey } from "#app/enums/stat"; export enum MoveCategory { PHYSICAL, @@ -800,10 +799,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) { @@ -811,10 +810,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) { @@ -1696,10 +1695,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; @@ -1721,7 +1719,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. @@ -1751,7 +1749,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)); @@ -2483,14 +2481,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; @@ -2538,18 +2536,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; } @@ -2561,8 +2557,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; } @@ -2570,7 +2566,7 @@ export class StatChangeAttr extends MoveEffectAttr { } getLevels(_user: Pokemon): integer { - return this.levels; + return this.stages; } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { @@ -2578,29 +2574,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); } @@ -2615,17 +2612,15 @@ export class StatChangeAttr extends MoveEffectAttr { } } -export class PostVictoryStatChangeAttr extends MoveAttr { +export class PostVictoryStatStageChangeAttr extends MoveAttr { private stats: BattleStat[]; private levels: integer; 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[], levels: integer, selfTarget?: boolean, condition?: MoveConditionFunc, showMessage: boolean = true, firstHitOnly: boolean = false) { super(); - this.stats = typeof(stats) === "number" - ? [ stats as BattleStat ] - : stats as BattleStat[]; + this.stats = stats; this.levels = levels; this.condition = condition!; // TODO: is this bang correct? this.showMessage = showMessage; @@ -2634,49 +2629,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.levels, 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; @@ -2697,7 +2691,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); } } @@ -2707,9 +2701,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 { @@ -2729,9 +2725,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(); @@ -2765,17 +2762,17 @@ 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 moves which swap the user and the target's stat stages. */ -export class SwapStatsAttr extends MoveEffectAttr { +export class SwapStatStagesAttr extends MoveEffectAttr { /** * Swaps the user and the target's stat changes. * @param user Pokemon that used the move @@ -2788,12 +2785,14 @@ export class SwapStatsAttr extends MoveEffectAttr { 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 + + let temp: number; + for (const s of BATTLE_STATS) { + temp = user.getStatStage(s); + user.setStatStage(s, target.getStatStage(s)); + target.setStatStage(s, temp); } + target.updateInfo(); user.updateInfo(); target.scene.queueMessage(i18next.t("moveTriggers:switchedStatChanges", {pokemonName: getPokemonNameWithAffix(user)})); @@ -3042,7 +3041,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 @@ -3053,7 +3052,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 ]; @@ -3077,7 +3076,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 @@ -3087,14 +3086,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; } } @@ -3318,14 +3317,14 @@ export class HitCountPowerAttr extends VariablePowerAttr { * @param {Pokemon} pokemon The pokemon that is being used to calculate the count of positive stats * @returns {number} 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 @@ -3335,9 +3334,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; } } @@ -3359,10 +3358,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; } @@ -3582,7 +3581,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; } } @@ -3593,7 +3592,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; } } @@ -3615,7 +3614,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; } } @@ -3737,7 +3736,7 @@ export class PhotonGeyserCategoryAttr extends VariableMoveCategoryAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const category = (args[0] as Utils.IntegerHolder); - 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; } @@ -3750,7 +3749,7 @@ export class TeraBlastCategoryAttr extends VariableMoveCategoryAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const category = (args[0] as Utils.IntegerHolder); - 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; } @@ -3787,8 +3786,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) { @@ -4423,8 +4422,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; } } @@ -4970,7 +4969,7 @@ 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); + const battleStatTotal = user.getStatStages().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)); } return ret; @@ -5788,8 +5787,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(); @@ -6186,7 +6194,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(), @@ -6222,7 +6230,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), @@ -6250,7 +6258,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) @@ -6263,13 +6271,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) @@ -6288,7 +6296,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), @@ -6313,9 +6321,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), @@ -6342,7 +6350,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) @@ -6369,7 +6377,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), @@ -6406,13 +6414,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(), @@ -6425,28 +6433,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), @@ -6494,17 +6502,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(), @@ -6541,7 +6549,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) @@ -6550,13 +6558,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) @@ -6582,7 +6590,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) @@ -6637,7 +6645,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) @@ -6652,21 +6660,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)) })); // TODO: BattleStats }), 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) @@ -6686,7 +6694,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) @@ -6710,13 +6718,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) @@ -6727,7 +6735,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) @@ -6781,7 +6789,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, @@ -6797,12 +6805,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) @@ -6829,7 +6837,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) @@ -6838,15 +6846,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), @@ -6885,13 +6893,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) @@ -6910,7 +6918,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(), @@ -6930,7 +6938,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) @@ -6974,14 +6982,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) @@ -7008,13 +7016,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), @@ -7026,33 +7034,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), @@ -7073,7 +7081,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) @@ -7085,25 +7093,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), @@ -7118,12 +7126,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) @@ -7137,7 +7145,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) @@ -7151,7 +7159,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) @@ -7176,7 +7184,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) @@ -7186,7 +7194,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) @@ -7246,7 +7254,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), 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) @@ -7263,7 +7271,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) @@ -7282,7 +7290,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(), @@ -7297,22 +7305,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) @@ -7335,7 +7343,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) @@ -7344,13 +7352,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) @@ -7360,7 +7368,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), @@ -7368,7 +7376,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) @@ -7390,7 +7398,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) @@ -7408,7 +7416,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(), @@ -7417,7 +7425,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(), @@ -7443,16 +7451,16 @@ 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), @@ -7469,7 +7477,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() @@ -7495,7 +7503,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) @@ -7512,13 +7520,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), @@ -7540,7 +7548,7 @@ export function initMoves() { 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), @@ -7552,8 +7560,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() @@ -7567,8 +7575,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) @@ -7599,10 +7607,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) @@ -7611,9 +7619,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) @@ -7631,7 +7639,7 @@ export function initMoves() { .attr(IgnoreOpponentStatChangesAttr) .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) @@ -7639,13 +7647,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) @@ -7674,14 +7682,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}"})) @@ -7692,14 +7700,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), @@ -7723,12 +7731,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() @@ -7737,7 +7745,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) @@ -7760,7 +7768,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) @@ -7774,7 +7782,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), @@ -7784,11 +7792,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), @@ -7798,12 +7806,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) @@ -7817,26 +7825,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 @@ -7851,7 +7859,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) @@ -7860,7 +7868,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) @@ -7891,9 +7899,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 */ @@ -8026,7 +8034,7 @@ export function initMoves() { .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) @@ -8034,8 +8042,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}"})) @@ -8047,11 +8055,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) @@ -8066,11 +8074,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); @@ -8096,7 +8104,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) @@ -8104,7 +8112,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), @@ -8138,7 +8146,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) @@ -8148,18 +8156,18 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES) .partial(), 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) @@ -8171,7 +8179,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) @@ -8213,14 +8221,14 @@ 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() .ignoresVirtual(), /* End Unused */ new AttackMove(Moves.ZIPPY_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 100, 2, 7) - .attr(StatChangeAttr, BattleStat.EVA, 1, true), + .attr(StatStageChangeAttr, [ Stat.EVA ], 1, true), new AttackMove(Moves.SPLISHY_SPLASH, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, 30, 0, 7) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -8281,17 +8289,17 @@ 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.TRAPPED, true, true, 1), 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) @@ -8386,15 +8394,15 @@ 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), 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), @@ -8407,25 +8415,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) @@ -8449,14 +8457,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) @@ -8477,7 +8485,7 @@ 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) .target(MoveTarget.ALL_NEAR_ENEMIES) .partial(), @@ -8490,7 +8498,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), @@ -8527,7 +8535,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), @@ -8539,18 +8547,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) @@ -8566,10 +8574,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) @@ -8577,15 +8585,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) @@ -8596,7 +8604,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) @@ -8616,7 +8624,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) @@ -8733,17 +8741,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() @@ -8782,24 +8790,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) @@ -8821,17 +8829,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) @@ -8840,7 +8848,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() @@ -8895,7 +8903,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) @@ -8948,7 +8956,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..3da405ab293 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.js"; 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(`-${getShortenedStatKey(decreasedStat)}`, TextStyle.SUMMARY_BLUE)}${getTextFrag(")", textStyle)}`; } else { ret = getTextFrag(`${ret}${!forStarterSelect ? "\n" : " "}(-)`, textStyle); } 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/enums/stat.ts b/src/enums/stat.ts index 32eb0d2a0b9..61a34a24ad4 100644 --- a/src/enums/stat.ts +++ b/src/enums/stat.ts @@ -20,8 +20,30 @@ export enum Stat { export const PERMANENT_STATS = [ Stat.HP, Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ] as const; export type PermanentStat = typeof PERMANENT_STATS[number]; +export const EFFECTIVE_STATS = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ] as const; +export type EffectiveStat = typeof EFFECTIVE_STATS[number]; + export const BATTLE_STATS = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD, Stat.ACC, Stat.EVA ] as const; export type BattleStat = typeof BATTLE_STATS[number]; export const TEMP_BATTLE_STATS = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD, Stat.ACC ] as const; export type TempBattleStat = typeof TEMP_BATTLE_STATS[number]; + +export function getStatStageChangeDescriptionKey(stages: integer, 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"; +} + +export function getStatKey(stat: Stat) { + return `pokemonInfo:Stat.${Stat[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 ddd2cfb0c9a..e8792a22ad1 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3,13 +3,13 @@ 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, StatusMoveTypeImmunityAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, MoveFlags, NeutralDamageAgainstFlyingTypeMultiplierAttr, OneHitKOAccuracyAttr } from "../data/move"; +import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, StatusMoveTypeImmunityAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, MoveFlags, NeutralDamageAgainstFlyingTypeMultiplierAttr, OneHitKOAccuracyAttr } 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 { BATTLE_STATS, EFFECTIVE_STATS, Stat } 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"; @@ -17,11 +17,11 @@ 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 { DamagePhase, FaintPhase, LearnMovePhase, MoveEffectPhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase, MoveEndPhase } from "../phases"; +import { DamagePhase, FaintPhase, LearnMovePhase, MoveEffectPhase, ObtainStatusEffectPhase, StatStageChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase, MoveEndPhase } from "../phases"; import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; 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 } from "../data/ability"; +import { Ability, AbAttr, StatStageMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStageChangesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatStageMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr } from "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; @@ -49,7 +49,7 @@ import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { getPokemonNameWithAffix } from "#app/messages.js"; -import { PermanentStat, BattleStat, PERMANENT_STATS } from "#app/enums/stat"; // TODO: Add type +import { PermanentStat, BattleStat, PERMANENT_STATS, EffectiveStat } from "#app/enums/stat"; // TODO: Add type export enum FieldPosition { CENTER, @@ -735,7 +735,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - getBattleStat(stat: PermanentStat & BattleStat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): integer { + getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): integer { const statLevel = new Utils.IntegerHolder(this.getStatStage(stat)); if (opponent) { if (isCritical) { @@ -750,7 +750,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { break; } } - applyAbAttrs(IgnoreOpponentStatChangesAbAttr, opponent, null, statLevel); + applyAbAttrs(IgnoreOpponentStatStageChangesAbAttr, opponent, null, statLevel); if (move) { applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, opponent, move, statLevel); } @@ -763,12 +763,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { 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); // TODO: BattleStat + applyStatStageMultiplierAbAttrs(StatStageMultiplierAbAttr, this, stat, statValue); let ret = statValue.value * (Math.max(2, 2 + statLevel.value) / Math.max(2, 2 - statLevel.value)); switch (stat) { case Stat.ATK: @@ -1374,7 +1374,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. @@ -1730,7 +1730,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)]); } @@ -1950,8 +1950,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const userAccStage = new Utils.IntegerHolder(this.getStatStage(Stat.ACC)); const targetEvaStage = new Utils.IntegerHolder(target.getStatStage(Stat.EVA)); - applyAbAttrs(IgnoreOpponentStatChangesAbAttr, target, null, userAccStage); - applyAbAttrs(IgnoreOpponentStatChangesAbAttr, this, null, targetEvaStage); + applyAbAttrs(IgnoreOpponentStatStageChangesAbAttr, target, null, userAccStage); + applyAbAttrs(IgnoreOpponentStatStageChangesAbAttr, this, null, targetEvaStage); applyAbAttrs(IgnoreOpponentEvasionAbAttr, this, null, targetEvaStage); applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, target, sourceMove, targetEvaStage); this.scene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), Stat.ACC, userAccStage); @@ -1967,10 +1967,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { : 3 / (3 + Math.min(targetEvaStage.value - userAccStage.value, 6)); } - applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, BattleStat.ACC, accuracyMultiplier, sourceMove); // TODO: BattleStat + applyStatStageMultiplierAbAttrs(StatStageMultiplierAbAttr, this, Stat.ACC, accuracyMultiplier, sourceMove); // TODO: BattleStat const evasionMultiplier = new Utils.NumberHolder(1); - applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, target, BattleStat.EVA, evasionMultiplier); // TODO: BattleStat + applyStatStageMultiplierAbAttrs(StatStageMultiplierAbAttr, target, Stat.EVA, evasionMultiplier); // TODO: BattleStat accuracyMultiplier.value /= evasionMultiplier.value; @@ -2087,8 +2087,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, criticalMultiplier); const screenMultiplier = new Utils.NumberHolder(1); @@ -2497,11 +2497,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 { - /** TODO: BattleStats - 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 yawn, and infatuation as those can not be passed via Baton Pass @@ -2511,11 +2515,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.summonData.tags.push(tag); } - /** TODO: BattleStats - if (this instanceof PlayerPokemon && source.summonData.battleStats.find(bs => bs === 6)) { - this.scene.validateAchv(achvs.TRANSFER_MAX_BATTLE_STAT); - } - */ + this.updateInfo(); } @@ -4161,45 +4161,46 @@ export class EnemyPokemon extends Pokemon { return true; } - handleBossSegmentCleared(segmentIndex: integer): void { // TODO: BattleStat + 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; - + // Increment the amount of stages the chosen stat will be raised + let stages = 1; switch (segmentIndex) { case 1: if (this.bossSegments >= 3) { - statLevels++; + stages++; } break; case 2: if (this.bossSegments >= 5) { - statLevels++; + stages++; } break; } - 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--; } } diff --git a/src/locales/ca_ES/achv.ts b/src/locales/ca_ES/achv.ts index a05c8b814ab..5c6fc8559e7 100644 --- a/src/locales/ca_ES/achv.ts +++ b/src/locales/ca_ES/achv.ts @@ -89,9 +89,9 @@ export const PGMachv: AchievementTranslationEntries = { name: "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", diff --git a/src/locales/de/achv.ts b/src/locales/de/achv.ts index 3d498d048a7..3e82b77d6de 100644 --- a/src/locales/de/achv.ts +++ b/src/locales/de/achv.ts @@ -90,7 +90,7 @@ export const PGMachv: AchievementTranslationEntries = { name: "Bänder-Meister", }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { name: "Teamwork", description: "Nutze Staffette, während der Anwender mindestens eines Statuswertes maximiert hat.", }, @@ -333,7 +333,7 @@ export const PGFachv: AchievementTranslationEntries = { name: "Bänder-Meisterin", }, - "TRANSFER_MAX_BATTLE_STAT": PGMachv.TRANSFER_MAX_BATTLE_STAT, + "TRANSFER_MAX_STAT_STAGE": PGMachv.TRANSFER_MAX_STAT_STAGE, "MAX_FRIENDSHIP": PGMachv.MAX_FRIENDSHIP, "MEGA_EVOLVE": PGMachv.MEGA_EVOLVE, "GIGANTAMAX": PGMachv.GIGANTAMAX, diff --git a/src/locales/de/pokemon-info.ts b/src/locales/de/pokemon-info.ts index b926c0c5115..08a23de2b3a 100644 --- a/src/locales/de/pokemon-info.ts +++ b/src/locales/de/pokemon-info.ts @@ -3,7 +3,6 @@ import { PokemonInfoTranslationEntries } from "#app/interfaces/locales"; export const pokemonInfo: PokemonInfoTranslationEntries = { Stat: { "HP": "KP", - "HPStat": "KP", "HPshortened": "KP", "ATK": "Angriff", "ATKshortened": "Ang", diff --git a/src/locales/en/achv.ts b/src/locales/en/achv.ts index a05c8b814ab..5c6fc8559e7 100644 --- a/src/locales/en/achv.ts +++ b/src/locales/en/achv.ts @@ -89,9 +89,9 @@ export const PGMachv: AchievementTranslationEntries = { name: "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", diff --git a/src/locales/en/pokemon-info.ts b/src/locales/en/pokemon-info.ts index 70a06294c76..95325bad297 100644 --- a/src/locales/en/pokemon-info.ts +++ b/src/locales/en/pokemon-info.ts @@ -3,7 +3,7 @@ import { PokemonInfoTranslationEntries } from "#app/interfaces/locales"; export const pokemonInfo: PokemonInfoTranslationEntries = { Stat: { "HP": "Max. HP", - "HPshortened": "MaxHP", + "HPshortened": "HP", "ATK": "Attack", "ATKshortened": "Atk", "DEF": "Defense", @@ -16,7 +16,6 @@ export const pokemonInfo: PokemonInfoTranslationEntries = { "SPDshortened": "Spd", "ACC": "Accuracy", "EVA": "Evasiveness", - "HPStat": "HP" }, Type: { diff --git a/src/locales/es/achv.ts b/src/locales/es/achv.ts index e291d93add2..0681cdd1285 100644 --- a/src/locales/es/achv.ts +++ b/src/locales/es/achv.ts @@ -89,7 +89,7 @@ export const PGMachv: AchievementTranslationEntries = { name: "Campeón 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.", }, diff --git a/src/locales/fr/achv.ts b/src/locales/fr/achv.ts index a2d7d2378a1..231b4e44ae8 100644 --- a/src/locales/fr/achv.ts +++ b/src/locales/fr/achv.ts @@ -89,7 +89,7 @@ export const PGMachv: AchievementTranslationEntries = { name: "Master Maitre 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", }, @@ -363,7 +363,7 @@ export const PGFachv: AchievementTranslationEntries = { name: "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/it/achv.ts b/src/locales/it/achv.ts index 91222b81579..2535a0725d3 100644 --- a/src/locales/it/achv.ts +++ b/src/locales/it/achv.ts @@ -89,7 +89,7 @@ export const PGMachv: AchievementTranslationEntries = { name: "Campione Lega Assoluta", }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { name: "Lavoro di Squadra", description: "Trasferisci almeno sei bonus statistiche tramite staffetta", }, diff --git a/src/locales/ja/achv.ts b/src/locales/ja/achv.ts index 9693a16cdc0..d86f58cee19 100644 --- a/src/locales/ja/achv.ts +++ b/src/locales/ja/achv.ts @@ -89,7 +89,7 @@ export const PGMachv: AchievementTranslationEntries = { name: "マスターリーグチャンピオン", }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { name: "同力", description: "少なくとも 一つの 能力を 最大まで あげて 他の 手持ちポケモンに バトンタッチする", }, diff --git a/src/locales/ko/achv.ts b/src/locales/ko/achv.ts index 431cb7d3296..2fa86495a75 100644 --- a/src/locales/ko/achv.ts +++ b/src/locales/ko/achv.ts @@ -89,7 +89,7 @@ export const PGMachv: AchievementTranslationEntries = { name: "마스터 리그 챔피언", }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { name: "팀워크", description: "한 개 이상의 능력치가 최대 랭크일 때 배턴터치 사용", }, diff --git a/src/locales/pt_BR/achv.ts b/src/locales/pt_BR/achv.ts index 59aae596f83..dffdbbbede4 100644 --- a/src/locales/pt_BR/achv.ts +++ b/src/locales/pt_BR/achv.ts @@ -89,7 +89,7 @@ export const PGMachv: AchievementTranslationEntries = { 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", }, @@ -364,7 +364,7 @@ export const PGFachv: AchievementTranslationEntries = { 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", }, diff --git a/src/locales/zh_CN/achv.ts b/src/locales/zh_CN/achv.ts index 303beeffea3..c33c7de1cc7 100644 --- a/src/locales/zh_CN/achv.ts +++ b/src/locales/zh_CN/achv.ts @@ -89,7 +89,7 @@ export const PGMachv: AchievementTranslationEntries = { name: "大师球联盟冠军", }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { name: "团队协作", description: "在一项属性强化至最大时用接力棒传递给其他宝可梦", }, diff --git a/src/locales/zh_CN/pokemon-info.ts b/src/locales/zh_CN/pokemon-info.ts index f7aaf3a33cb..84b6e6d03fc 100644 --- a/src/locales/zh_CN/pokemon-info.ts +++ b/src/locales/zh_CN/pokemon-info.ts @@ -3,7 +3,7 @@ import { PokemonInfoTranslationEntries } from "#app/interfaces/locales"; export const pokemonInfo: PokemonInfoTranslationEntries = { Stat: { "HP": "最大HP", - "HPshortened": "最大HP", + "HPshortened": "HP", "ATK": "攻击", "ATKshortened": "攻击", "DEF": "防御", diff --git a/src/locales/zh_TW/achv.ts b/src/locales/zh_TW/achv.ts index dbd1d44d220..0b3b39c329b 100644 --- a/src/locales/zh_TW/achv.ts +++ b/src/locales/zh_TW/achv.ts @@ -89,7 +89,7 @@ export const PGMachv: AchievementTranslationEntries = { name: "大師球聯盟冠軍", }, - "TRANSFER_MAX_BATTLE_STAT": { + "TRANSFER_MAX_STAT_STAGE": { name: "團隊協作", description: "在一項屬性強化至最大時用接力棒傳遞給其他寶可夢", }, diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 93ebee3d7a8..f0ef4c73ed6 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -26,7 +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 } from "#app/enums/stat"; +import { PermanentStat, TEMP_BATTLE_STATS, TempBattleStat, Stat, getStatKey } from "#app/enums/stat"; const outputModifierData = false; const useMaxWeightForOutput = false; @@ -443,7 +443,7 @@ export class TempStatStageBoosterModifierType extends ModifierType implements Ge } getDescription(_scene: BattleScene): string { - return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { stat: i18next.t(`pokemonInfo:Stat.${Stat[this.stat]}`) }); + return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { stat: i18next.t(getStatKey(this.stat)) }); } getPregenArgs(): any[] { @@ -609,7 +609,7 @@ export class BaseStatBoosterModifierType extends PokemonHeldItemModifierType imp } getDescription(_scene: BattleScene): string { - return i18next.t("modifierType:ModifierType.BaseStatBoosterModifierType.description", { stat: i18next.t(`pokemonInfo:Stat.${Stat[this.stat]}`) }); + return i18next.t("modifierType:ModifierType.BaseStatBoosterModifierType.description", { stat: i18next.t(getStatKey(this.stat)) }); } getPregenArgs(): any[] { diff --git a/src/phases.ts b/src/phases.ts index a9ec6791f8f..298f8065c3f 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1,10 +1,9 @@ import BattleScene, { bypassLogin } from "./battle-scene"; import { default as Pokemon, PlayerPokemon, EnemyPokemon, PokemonMove, MoveResult, DamageResult, FieldPosition, HitResult, TurnMove } from "./field/pokemon"; import * as Utils from "./utils"; -import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr, MoveHeaderAttr } from "./data/move"; +import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatStageChangeAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr, MoveHeaderAttr } from "./data/move"; import { Mode } from "./ui/ui"; import { Command } from "./ui/command-ui-handler"; -import { Stat } from "./data/pokemon-stat"; import { BerryModifier, ContactHeldItemTransferChanceModifier, EnemyAttackStatusEffectChanceModifier, EnemyPersistentModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, LapsingPersistentModifier, MapModifier, Modifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, SwitchEffectTransferModifier, TurnHealModifier, TurnHeldItemTransferModifier, MoneyMultiplierModifier, MoneyInterestModifier, IvScannerModifier, LapsingPokemonHeldItemModifier, PokemonMultiHitModifier, overrideModifiers, overrideHeldItems, BypassSpeedChanceModifier, TurnStatusEffectModifier, ResetNegativeStatStageModifier } from "./modifier/modifier"; import PartyUiHandler, { PartyOption, PartyUiMode } from "./ui/party-ui-handler"; import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "./data/pokeball"; @@ -14,7 +13,6 @@ import { SummaryUiMode } from "./ui/summary-ui-handler"; import EvolutionSceneHandler from "./ui/evolution-scene-handler"; import { EvolutionPhase } from "./evolution-phase"; import { Phase } from "./phase"; -import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "./data/battle-stat"; import { biomeLinks, getBiomeName } from "./data/biomes"; import { ModifierTier } from "./modifier/modifier-tier"; import { FusePokemonModifierType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeOption, PokemonModifierType, PokemonMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, RememberMoveModifierType, TmModifierType, getDailyRunStarterModifiers, getEnemyBuffModifierForWave, getModifierType, getPlayerModifierTypeOptions, getPlayerShopModifierTypeOptionsForWave, modifierTypes, regenerateModifierPoolThresholds } from "./modifier/modifier-type"; @@ -25,7 +23,7 @@ import { Starter } from "./ui/starter-select-ui-handler"; import { Gender } from "./data/gender"; import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather"; import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag"; -import { CheckTrappedAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr } from "./data/ability"; +import { CheckTrappedAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatStageChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatStageChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatStageChangeAbAttr, applyPostStatStageChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatStageChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./field/arena"; import { BattleType, BattlerIndex, TurnCommand } from "./battle"; @@ -66,6 +64,7 @@ import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; import { applyChallenges, ChallengeType } from "./data/challenge"; import { pokemonEvolutions } from "./data/pokemon-evolutions"; +import { Stat, BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat"; const { t } = i18next; @@ -718,8 +717,8 @@ export abstract class FieldPhase extends BattlePhase { }, 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; }); @@ -3449,22 +3448,22 @@ export class ShowAbilityPhase extends PokemonPhase { export type StatChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void; -export class StatChangePhase extends PokemonPhase { +export class StatStageChangePhase extends PokemonPhase { // TODO: BattleStat private stats: BattleStat[]; private selfTarget: boolean; - private levels: integer; + private stages: 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) { + constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], stages: 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.stages = stages; this.showMessage = showMessage; this.ignoreAbilities = ignoreAbilities; this.canBeCopied = canBeCopied; @@ -3490,21 +3489,21 @@ export class StatChangePhase extends PokemonPhase { 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) { + if (!this.selfTarget && this.stages < 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); + if (!cancelled.value && !this.selfTarget && this.stages < 0) { + applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, this.getPokemon(), stat, cancelled); } return !cancelled.value; }); - const levels = new Utils.IntegerHolder(this.levels); + const levels = new Utils.IntegerHolder(this.stages); if (!this.ignoreAbilities) { - applyAbAttrs(StatChangeMultiplierAbAttr, pokemon, null, levels); + applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, levels); } const battleStats = this.getPokemon().summonData.battleStats; @@ -3526,20 +3525,20 @@ export class StatChangePhase extends PokemonPhase { if (levels.value > 0 && this.canBeCopied) { for (const opponent of pokemon.getOpponents()) { - applyAbAttrs(StatChangeCopyAbAttr, opponent, null, this.stats, levels.value); + applyAbAttrs(StatStageChangeCopyAbAttr, opponent, null, this.stats, levels.value); } } - applyPostStatChangeAbAttrs(PostStatChangeAbAttr, pokemon, filteredStats, this.levels, this.selfTarget); + applyPostStatStageChangeAbAttrs(PostStatStageChangeAbAttr, 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)) { + 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 PokemonResetNegativeStatStageModifier; + const whiteHerb = this.scene.applyModifier(ResetNegativeStatStageModifier, this.player, pokemon) as ResetNegativeStatStageModifier; // If the White Herb was applied, consume it if (whiteHerb) { - --whiteHerb.stackCount; + whiteHerb.stackCount--; if (whiteHerb.stackCount <= 0) { this.scene.removeModifier(whiteHerb); } @@ -3563,7 +3562,7 @@ export class StatChangePhase extends PokemonPhase { // 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 spriteColor = levels.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); @@ -3657,11 +3656,19 @@ export class StatChangePhase extends PokemonPhase { 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)); + : `${relLevelStats.slice(0, -1).map(s => i18next.t(getStatKey(s))).join(", ")}${relLevelStats.length > 2 ? "," : ""} ${i18next.t("battle:statsAnd")} ${i18next.t(getStatKey(relLevelStats[relLevelStats.length - 1]))}`; + messages.push(i18next.t(getStatStageChangeDescriptionKey(Math.abs(parseInt(rl)), levels >= 1), { + pokemonNameWithAffix: getPokemonNameWithAffix(this.getPokemon()), + stats: statsFragment, + count: relLevelStats.length + })); } else { - statsFragment = getBattleStatName(relLevelStats[0]); - messages.push(getBattleStatLevelChangeDescription(getPokemonNameWithAffix(this.getPokemon()), statsFragment, Math.abs(parseInt(rl)), levels >= 1,relLevelStats.length)); + statsFragment = i18next.t(getStatKey(relLevelStats[0])); + messages.push(i18next.t(getStatStageChangeDescriptionKey(Math.abs(parseInt(rl)), levels >= 1), { + pokemonNameWithAffix: getPokemonNameWithAffix(this.getPokemon()), + stats: statsFragment, + count: relLevelStats.length + })); } }); @@ -3985,7 +3992,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/system/achv.ts b/src/system/achv.ts index 040a48d9a7e..786c519f936 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -8,6 +8,7 @@ import { PlayerGender } from "#enums/player-gender"; import { ParseKeys } from "i18next"; import { Challenge, FreshStartChallenge, SingleGenerationChallenge, SingleTypeChallenge } from "#app/data/challenge.js"; import { ConditionFn } from "#app/@types/common.js"; +import { getShortenedStatKey, Stat } from "#app/enums/stat.js"; export enum AchvTier { COMMON, @@ -178,13 +179,13 @@ export function getAchievementDescription(localizationKey: string): string { case "10000_DMG": return i18next.t(`${genderPrefix}achv:DamageAchv.description` as ParseKeys, {"damageAmount": achvs._10000_DMG.damageAmount.toLocaleString("en-US")}); case "250_HEAL": - return i18next.t(`${genderPrefix}achv:HealAchv.description` as ParseKeys, {"healAmount": achvs._250_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")}); + return i18next.t(`${genderPrefix}achv:HealAchv.description` as ParseKeys, {"healAmount": achvs._250_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP)) }); case "1000_HEAL": - return i18next.t(`${genderPrefix}achv:HealAchv.description` as ParseKeys, {"healAmount": achvs._1000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")}); + return i18next.t(`${genderPrefix}achv:HealAchv.description` as ParseKeys, {"healAmount": achvs._1000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP)) }); case "2500_HEAL": - return i18next.t(`${genderPrefix}achv:HealAchv.description` as ParseKeys, {"healAmount": achvs._2500_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")}); + return i18next.t(`${genderPrefix}achv:HealAchv.description` as ParseKeys, {"healAmount": achvs._2500_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP)) }); case "10000_HEAL": - return i18next.t(`${genderPrefix}achv:HealAchv.description` as ParseKeys, {"healAmount": achvs._10000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")}); + return i18next.t(`${genderPrefix}achv:HealAchv.description` as ParseKeys, {"healAmount": achvs._10000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP)) }); case "LV_100": return i18next.t(`${genderPrefix}achv:LevelAchv.description` as ParseKeys, {"level": achvs.LV_100.level}); case "LV_250": @@ -201,8 +202,8 @@ export function getAchievementDescription(localizationKey: string): string { return i18next.t(`${genderPrefix}achv:RibbonAchv.description` as ParseKeys, {"ribbonAmount": achvs._75_RIBBONS.ribbonAmount.toLocaleString("en-US")}); case "100_RIBBONS": return i18next.t(`${genderPrefix}achv:RibbonAchv.description` as ParseKeys, {"ribbonAmount": achvs._100_RIBBONS.ribbonAmount.toLocaleString("en-US")}); - case "TRANSFER_MAX_BATTLE_STAT": - return i18next.t(`${genderPrefix}achv:TRANSFER_MAX_BATTLE_STAT.description` as ParseKeys); + case "TRANSFER_MAX_STAT_STAGE": + return i18next.t(`${genderPrefix}achv:TRANSFER_MAX_STAT_STAGE.description` as ParseKeys); case "MAX_FRIENDSHIP": return i18next.t(`${genderPrefix}achv:MAX_FRIENDSHIP.description` as ParseKeys); case "MEGA_EVOLVE": @@ -309,7 +310,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/test/abilities/costar.test.ts b/src/test/abilities/costar.test.ts index ef3fb3a2ab0..c1f4071c718 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.js"; +import { Stat } from "#enums/stat"; import { Abilities } from "#app/enums/abilities.js"; import { Moves } from "#app/enums/moves.js"; import { Species } from "#app/enums/species.js"; @@ -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.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); 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.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, 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/gulp_missile.test.ts b/src/test/abilities/gulp_missile.test.ts index 2647f765f6e..c13c0904dcd 100644 --- a/src/test/abilities/gulp_missile.test.ts +++ b/src/test/abilities/gulp_missile.test.ts @@ -13,7 +13,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 { BattleStat } from "#app/data/battle-stat.js"; +import { Stat } from "#enums/stat"; import { StatusEffect } from "#app/enums/status-effect.js"; import Pokemon from "#app/field/pokemon.js"; @@ -95,7 +95,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]); @@ -127,7 +127,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]); @@ -146,7 +146,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); }); @@ -207,7 +207,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 dde310fda2a..6f951537046 100644 --- a/src/test/abilities/hustle.test.ts +++ b/src/test/abilities/hustle.test.ts @@ -41,13 +41,13 @@ describe("Abilities - Hustle", () => { const pikachu = game.scene.getPlayerPokemon()!; const atk = pikachu.stats[Stat.ATK]; - vi.spyOn(pikachu, "getBattleStat"); + vi.spyOn(pikachu, "getEffectiveStat"); game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE)); await game.move.forceHit(); await game.phaseInterceptor.to(DamagePhase); - expect(pikachu.getBattleStat).toHaveReturnedWith(atk * 1.5); + expect(pikachu.getEffectiveStat).toHaveReturnedWith(atk * 1.5); }); it("lowers the accuracy of the user's physical moves by 20%", async () => { @@ -67,13 +67,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.doAttack(getMovePosition(game.scene, 0, 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/intimidate.test.ts b/src/test/abilities/intimidate.test.ts index 2c2b68bc5df..287a049a337 100644 --- a/src/test/abilities/intimidate.test.ts +++ b/src/test/abilities/intimidate.test.ts @@ -2,12 +2,9 @@ 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 { BattleStat } from "#app/data/battle-stat"; -import { generateStarter, getMovePosition } from "#test/utils/gameManagerUtils"; -import { Command } from "#app/ui/command-ui-handler"; -import { Status, StatusEffect } from "#app/data/status-effect"; -import { GameModes, getGameMode } from "#app/game-mode"; -import { CommandPhase, DamagePhase, EncounterPhase, EnemyCommandPhase, SelectStarterPhase, TurnInitPhase } from "#app/phases"; +import { Stat } from "#enums/stat"; +import { getMovePosition } from "#test/utils/gameManagerUtils"; +import { CommandPhase, TurnInitPhase } from "#app/phases"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -38,7 +35,7 @@ describe("Abilities - Intimidate", () => { game.override.enemyMoveset(SPLASH_ONLY); }); - it("single - wild with switch", async () => { + it("should lower ATK stat stage by 1 of enemy Pokemon on entry and player switch", async () => { await game.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); game.onNextPrompt( "CheckSwitchPhase", @@ -50,108 +47,25 @@ describe("Abilities - Intimidate", () => { () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase) ); await game.phaseInterceptor.to(CommandPhase, false); - expect(game.scene.getParty()[0].species.speciesId).toBe(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); + + let playerPokemon = game.scene.getPlayerPokemon(); + const enemyPokemon = game.scene.getEnemyPokemon(); + + expect(playerPokemon!.species.speciesId).toBe(Species.MIGHTYENA); + expect(enemyPokemon!.getStatStage(Stat.ATK)).toBe(-1); + expect(playerPokemon!.getStatStage(Stat.ATK)).toBe(-1); + game.doSwitchPokemon(1); await game.phaseInterceptor.run(CommandPhase); await game.phaseInterceptor.to(CommandPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.POOCHYENA); - 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); + 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("single - boss should only trigger once then switch", async () => { - game.override.startingWave(10); - await game.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); - 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.run(CommandPhase); - await game.phaseInterceptor.to(CommandPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.POOCHYENA); - - 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.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); - 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.run(CommandPhase); - await game.phaseInterceptor.to(CommandPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.POOCHYENA); - - 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"); - game.override.startingWave(5); - await game.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(-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 () => { + it("should lower ATK stat stage by 1 for every enemy Pokemon in a double battle on entry", async () => { game.override.battleType("double"); game.override.startingWave(3); await game.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); @@ -165,210 +79,60 @@ describe("Abilities - Intimidate", () => { () => 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(-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 playerField = game.scene.getPlayerField()!; + const enemyField = game.scene.getEnemyPokemon()!; - const battleStatsPokemon2 = game.scene.getParty()[1].summonData.battleStats; - expect(battleStatsPokemon2[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("double - boss: should only trigger once per pokemon", async () => { - game.override.battleType("double"); - game.override.startingWave(10); - await game.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(-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 () => { - game.override.startingWave(2); - game.override.moveset([Moves.AERIAL_ACE]); - 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.onNextPrompt("CommandPhase", Mode.COMMAND, () => { - game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); - }); - game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { - const movePosition = getMovePosition(game.scene, 0, Moves.AERIAL_ACE); - (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); - }); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(DamagePhase); - await game.killPokemon(game.scene.currentBattle.enemyParty[0]); - expect(game.scene.currentBattle.enemyParty[0].isFainted()).toBe(true); - 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 () => { + 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.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.onNextPrompt("CommandPhase", Mode.COMMAND, () => { - game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); - }); - game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { - const movePosition = getMovePosition(game.scene, 0, Moves.AERIAL_ACE); - (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); - }); - console.log("===to new turn==="); + 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.doAttack(getMovePosition(game.scene, 0, 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); + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); }, 20000); - it("single - trainer should only trigger once and each time he switch", async () => { + it("should lower ATK stat stage by 1 for every switch", async () => { game.override.moveset([Moves.SPLASH]); game.override.enemyMoveset([Moves.VOLT_SWITCH, Moves.VOLT_SWITCH, Moves.VOLT_SWITCH, Moves.VOLT_SWITCH]); 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.onNextPrompt("CommandPhase", Mode.COMMAND, () => { - game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); - }); - game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { - const movePosition = getMovePosition(game.scene, 0, Moves.AERIAL_ACE); - (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); - }); - console.log("===to new turn==="); - 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); + const playerPokemon = game.scene.getPlayerPokemon()!; + let enemyPokemon = game.scene.getEnemyPokemon()!; - game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { - game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); - }); - game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { - const movePosition = getMovePosition(game.scene, 0, Moves.AERIAL_ACE); - (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); - }); - console.log("===to new turn==="); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); + + game.doAttack(getMovePosition(game.scene, 0, 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); + + enemyPokemon = game.scene.getEnemyPokemon()!; + + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-2); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + await game.toNextTurn(); + + enemyPokemon = game.scene.getEnemyPokemon()!; + + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-3); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); }, 200000); - - it("single - trainer should only trigger once whatever turn we are", async () => { - game.override.moveset([Moves.SPLASH]); - game.override.enemyMoveset(SPLASH_ONLY); - 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.onNextPrompt("CommandPhase", Mode.COMMAND, () => { - game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); - }); - game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { - const movePosition = getMovePosition(game.scene, 0, Moves.AERIAL_ACE); - (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); - }); - console.log("===to new turn==="); - 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); - - game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { - game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); - }); - game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { - const movePosition = getMovePosition(game.scene, 0, Moves.AERIAL_ACE); - (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); - }); - console.log("===to new turn==="); - 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("double - wild vs only 1 on player side", async () => { - game.override.battleType("double"); - game.override.startingWave(3); - await game.runToSummon([Species.MIGHTYENA]); - await game.phaseInterceptor.to(CommandPhase, false); - const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); - 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); - }, 20000); - - it("double - wild vs only 1 alive on player side", async () => { - game.override.battleType("double"); - game.override.startingWave(3); - 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); - - await game.phaseInterceptor.to(CommandPhase, false); - const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); - 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); - }, 20000); }); diff --git a/src/test/abilities/intrepid_sword.test.ts b/src/test/abilities/intrepid_sword.test.ts index bc83c9bb44b..a21fb85b402 100644 --- a/src/test/abilities/intrepid_sword.test.ts +++ b/src/test/abilities/intrepid_sword.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { CommandPhase } from "#app/phases"; import GameManager from "#test/utils/gameManager"; import { Abilities } from "#enums/abilities"; @@ -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.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/moxie.test.ts b/src/test/abilities/moxie.test.ts index f99068dea41..4108f00ad5a 100644 --- a/src/test/abilities/moxie.test.ts +++ b/src/test/abilities/moxie.test.ts @@ -1,16 +1,14 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { Stat } from "#app/data/pokemon-stat"; -import { CommandPhase, EnemyCommandPhase, VictoryPhase } from "#app/phases"; +import { Stat } from "#enums/stat"; +import { EnemyCommandPhase, TurnEndPhase, VictoryPhase } from "#app/phases"; import GameManager from "#test/utils/gameManager"; import { getMovePosition } from "#test/utils/gameManagerUtils"; -import { Command } from "#app/ui/command-ui-handler"; -import { Mode } from "#app/ui/ui"; 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 } from "vitest"; - +import { SPLASH_ONLY } from "../utils/testUtils"; +import { BattlerIndex } from "#app/battle"; describe("Abilities - Moxie", () => { let phaserGame: Phaser.Game; @@ -34,29 +32,46 @@ 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 defeating an enemy Pokemon", 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()!; - game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { - game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); - }); - game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { - const movePosition = getMovePosition(game.scene, 0, moveToUse); - (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); - }); + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0); + + game.doAttack(getMovePosition(game.scene, 0, 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); + + it("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.doAttack(getMovePosition(game.scene, 0, moveToUse)); + game.doSelectTarget(BattlerIndex.PLAYER_2); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(firstPokemon.getStatStage(Stat.ATK)).toBe(1); }, 20000); }); diff --git a/src/test/abilities/parental_bond.test.ts b/src/test/abilities/parental_bond.test.ts index 182f780763c..53376bd3b48 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.js"; +import { Stat } from "#enums/stat"; import { StatusEffect } from "#app/data/status-effect.js"; import { Type } from "#app/data/type.js"; import { BattlerTagType } from "#app/enums/battler-tag-type.js"; @@ -91,7 +91,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 ); @@ -111,7 +111,7 @@ describe("Abilities - Parental Bond", () => { game.doAttack(getMovePosition(game.scene, 0, 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 ); @@ -563,7 +563,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 ); @@ -585,7 +585,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 6aab362634a..e0168b447db 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.js"; -import { BattleStat } from "#app/data/battle-stat.js"; +import { StatStageMultiplierAbAttr, allAbilities } from "#app/data/ability.js"; +import { Stat } from "#enums/stat"; import { WeatherType } from "#app/data/weather.js"; import { CommandPhase, MoveEffectPhase, MoveEndPhase } from "#app/phases.js"; import { Abilities } from "#enums/abilities"; @@ -48,10 +48,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, battleStat, statValue, args) => { - if (battleStat === BattleStat.EVA && game.scene.arena.weather?.weatherType === WeatherType.SANDSTORM) { + const sandVeilAttr = allAbilities[Abilities.SAND_VEIL].getAttrs(StatStageMultiplierAbAttr)[0]; + vi.spyOn(sandVeilAttr, "applyStatStage").mockImplementation( + (_pokemon, _passive, 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 6fbe57978e9..d9db3239cda 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.js"; +import { Stat } from "#enums/stat"; import { TerrainType } from "#app/data/terrain.js"; import { MoveEndPhase, TurnEndPhase } from "#app/phases"; import GameManager from "#test/utils/gameManager"; @@ -9,6 +9,7 @@ import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; 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.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, 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/volt_absorb.test.ts b/src/test/abilities/volt_absorb.test.ts index 985459e133b..740141bd872 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.js"; +import { Stat } from "#enums/stat"; import { TurnEndPhase } from "#app/phases"; import GameManager from "#test/utils/gameManager"; import { getMovePosition } from "#test/utils/gameManagerUtils"; @@ -42,12 +42,14 @@ describe("Abilities - Volt Absorb", () => { await game.startBattle(); + const playerPokemon = game.scene.getPlayerPokemon()!; + game.doAttack(getMovePosition(game.scene, 0, 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 e27349efe41..5b0f00427a7 100644 --- a/src/test/abilities/wind_rider.test.ts +++ b/src/test/abilities/wind_rider.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat.js"; +import { Stat } from "#enums/stat"; import { TurnEndPhase } from "#app/phases"; import GameManager from "#test/utils/gameManager"; import { getMovePosition } from "#test/utils/gameManagerUtils"; @@ -32,37 +32,37 @@ describe("Abilities - Wind Rider", () => { game.override.enemyMoveset(SPLASH_ONLY); }); - it("takes no damage from wind moves and its Attack is increased by one stage when hit by one", async () => { + it("takes no damage from wind moves and its ATK stat stage is raised by 1 when hit by one", async () => { await game.startBattle([Species.MAGIKARP]); const shiftry = game.scene.getEnemyPokemon()!; - expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0); + expect(shiftry.getStatStage(Stat.ATK)).toBe(0); game.doAttack(getMovePosition(game.scene, 0, 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 () => { + it("ATK stat stage is raised by 1 when Tailwind is present on its side", async () => { game.override.ability(Abilities.WIND_RIDER); game.override.enemySpecies(Species.MAGIKARP); await game.startBattle([Species.SHIFTRY]); const shiftry = game.scene.getPlayerPokemon()!; - expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0); + expect(shiftry.getStatStage(Stat.ATK)).toBe(0); game.doAttack(getMovePosition(game.scene, 0, 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 () => { + it("does not raise ATK stat stage when Tailwind is present on opposing side", async () => { game.override.ability(Abilities.WIND_RIDER); game.override.enemySpecies(Species.MAGIKARP); @@ -70,33 +70,33 @@ 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.doAttack(getMovePosition(game.scene, 0, 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 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); await game.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); + expect(shiftry.getStatStage(Stat.ATK)).toBe(0); + expect(magikarp.getStatStage(Stat.ATK)).toBe(0); game.doAttack(getMovePosition(game.scene, 0, 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 interact with Sandstorm", async () => { @@ -105,14 +105,14 @@ describe("Abilities - Wind Rider", () => { await game.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.doAttack(getMovePosition(game.scene, 0, 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/achievements/achievement.test.ts b/src/test/achievements/achievement.test.ts index 5cd9c4d4094..387c581ce74 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 index 775dd40ff34..76040094235 100644 --- a/src/test/battle-stat.spec.ts +++ b/src/test/battle-stat.spec.ts @@ -1,8 +1,8 @@ -import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "#app/data/battle-stat.js"; +import { BattleStat, getStatKey } from "#enums/stat"; import { describe, expect, it } from "vitest"; import { arrayOfRange, mockI18next } from "./utils/testUtils"; -const TEST_BATTLE_STAT = -99 as unknown as BattleStat; +const TEST_BATTLE_STAT = -99 as BattleStat; const TEST_POKEMON = "Testmon"; const TEST_STAT = "Teststat"; @@ -25,7 +25,7 @@ describe("battle-stat", () => { }); it("should fall back to ??? for an unknown BattleStat", () => { - expect(getBattleStatName(TEST_BATTLE_STAT)).toBe("???"); + expect(getStatKey(TEST_BATTLE_STAT)).toBe("???"); }); }); diff --git a/src/test/battle/battle.test.ts b/src/test/battle/battle.test.ts index a4713f90506..3a434b17228 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.js"; +import { Stat } from "#enums/stat"; import { GameModes } from "#app/game-mode"; import { getGameMode } from "#app/game-mode.js"; import { @@ -339,7 +339,7 @@ describe("Test Battle Phase", () => { .startingLevel(100) .moveset([moveToUse]) .enemyMoveset(SPLASH_ONLY) - .startingHeldItems([{ name: "TEMP_STAT_BOOSTER", type: TempBattleStat.ACC }]); + .startingHeldItems([{ name: "TEMP_STAT_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 369a84e21fa..7d49850c584 100644 --- a/src/test/battlerTags/octolock.test.ts +++ b/src/test/battlerTags/octolock.test.ts @@ -3,14 +3,14 @@ import Pokemon from "#app/field/pokemon.js"; import BattleScene from "#app/battle-scene.js"; import { BattlerTag, BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags.js"; import { StatChangePhase } from "#app/phases.js"; -import { BattleStat } from "#app/data/battle-stat.js"; import { BattlerTagType } from "#app/enums/battler-tag-type.js"; +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 StatChangePhase with expected stat stage changes", { timeout: 10000 }, async () => { const mockPokemon = { scene: new BattleScene(), getBattlerIndex: () => 0, @@ -20,8 +20,8 @@ describe("BattlerTag - OctolockTag", () => { 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 as StatChangePhase)["stages"]).toEqual(-1); + expect((phase as StatChangePhase)["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 005f1e1593c..340c9528633 100644 --- a/src/test/battlerTags/stockpiling.test.ts +++ b/src/test/battlerTags/stockpiling.test.ts @@ -3,7 +3,7 @@ import Pokemon, { PokemonSummonData } from "#app/field/pokemon.js"; import BattleScene from "#app/battle-scene.js"; import { StockpilingTag } from "#app/data/battler-tags.js"; import { StatChangePhase } from "#app/phases.js"; -import { BattleStat } from "#app/data/battle-stat.js"; +import { Stat } from "#enums/stat"; import * as messages from "#app/messages.js"; beforeEach(() => { @@ -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 StatChangePhase with expected stat stage changes on add", { timeout: 10000 }, async () => { const mockPokemon = { scene: vi.mocked(new BattleScene()) as BattleScene, getBattlerIndex: () => 0, @@ -24,10 +24,10 @@ describe("BattlerTag - StockpilingTag", () => { 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 as StatChangePhase)["stages"]).toEqual(1); + expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ])); - (phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.DEF, BattleStat.SPDEF], [1, 1]); + (phase as StatChangePhase)["onChange"]!(mockPokemon, [Stat.DEF, Stat.SPDEF], [1, 1]); }); subject.onAdd(mockPokemon); @@ -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.setStatStage(Stat.DEF, 6); + mockPokemon.setStatStage(Stat.SPDEF, 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 as StatChangePhase)["stages"]).toEqual(1); + expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([Stat.DEF, Stat.SPDEF])); - (phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.DEF, BattleStat.SPDEF], [1, 1]); + (phase as StatChangePhase)["onChange"]!(mockPokemon, [ Stat.DEF, Stat.SPDEF ], [1, 1]); }); subject.onAdd(mockPokemon); @@ -76,10 +76,10 @@ describe("BattlerTag - StockpilingTag", () => { 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 as StatChangePhase)["stages"]).toEqual(1); + expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ])); - (phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.DEF, BattleStat.SPDEF], [1, 1]); + (phase as StatChangePhase)["onChange"]!(mockPokemon, [ Stat.DEF, Stat.SPDEF ], [1, 1]); }); subject.onOverlap(mockPokemon); @@ -98,18 +98,18 @@ describe("BattlerTag - StockpilingTag", () => { vi.spyOn(mockPokemon.scene, "queueMessage").mockImplementation(() => {}); - mockPokemon.summonData.battleStats[BattleStat.DEF] = 5; - mockPokemon.summonData.battleStats[BattleStat.SPDEF] = 4; + mockPokemon.setStatStage(Stat.DEF, 5); + mockPokemon.setStatStage(Stat.SPDEF, 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 as StatChangePhase)["stages"]).toEqual(1); + expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ])); // def doesn't change - (phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.SPDEF], [1]); + (phase as StatChangePhase)["onChange"]!(mockPokemon, [ Stat.SPDEF ], [1]); }); subject.onAdd(mockPokemon); @@ -117,11 +117,11 @@ describe("BattlerTag - StockpilingTag", () => { 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 as StatChangePhase)["stages"]).toEqual(1); + expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ])); // def doesn't change - (phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.SPDEF], [1]); + (phase as StatChangePhase)["onChange"]!(mockPokemon, [ Stat.SPDEF ], [1]); }); subject.onOverlap(mockPokemon); @@ -129,8 +129,8 @@ describe("BattlerTag - StockpilingTag", () => { 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 as StatChangePhase)["stages"]).toEqual(1); + expect((phase as StatChangePhase)["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 as StatChangePhase)["stages"]).toEqual(-2); + expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([Stat.SPDEF])); }); subject.onRemove(mockPokemon); diff --git a/src/test/items/eviolite.test.ts b/src/test/items/eviolite.test.ts index 0fe90866de8..88f225d1b5f 100644 --- a/src/test/items/eviolite.test.ts +++ b/src/test/items/eviolite.test.ts @@ -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 consoe 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/light_ball.test.ts b/src/test/items/light_ball.test.ts index ff7dfa4eba5..61ac99d7910 100644 --- a/src/test/items/light_ball.test.ts +++ b/src/test/items/light_ball.test.ts @@ -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 consoe 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 966762e4175..1ce24446757 100644 --- a/src/test/items/metal_powder.test.ts +++ b/src/test/items/metal_powder.test.ts @@ -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 consoe 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 d2435dab431..6f165667fad 100644 --- a/src/test/items/quick_powder.test.ts +++ b/src/test/items/quick_powder.test.ts @@ -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 consoe 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/thick_club.test.ts b/src/test/items/thick_club.test.ts index 841cd7c90ac..c805626790c 100644 --- a/src/test/items/thick_club.test.ts +++ b/src/test/items/thick_club.test.ts @@ -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 consoe 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/moves/baton_pass.test.ts b/src/test/moves/baton_pass.test.ts index 9f0cb3619b2..49c42729580 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.js"; +import { Stat } from "#enums/stat"; import { PostSummonPhase, TurnEndPhase } from "#app/phases.js"; import GameManager from "#app/test/utils/gameManager"; import { getMovePosition } from "#app/test/utils/gameManagerUtils"; @@ -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.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, Moves.BATON_PASS)); @@ -53,16 +56,16 @@ 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() => { // arrange game.override .startingWave(5) - .enemyMoveset(new Array(4).fill([Moves.NASTY_PLOT])); + .enemyMoveset([ Moves.NASTY_PLOT, Moves.NASTY_PLOT, Moves.NASTY_PLOT, Moves.NASTY_PLOT ]); await game.startBattle([ Species.RAICHU, Species.SHUCKLE @@ -74,13 +77,13 @@ describe("Moves - Baton Pass", () => { // round 2 - baton pass game.scene.getEnemyPokemon()!.hp = 100; - game.override.enemyMoveset(new Array(4).fill(Moves.BATON_PASS)); + game.override.enemyMoveset([ Moves.BATON_PASS, Moves.BATON_PASS, Moves.BATON_PASS, Moves.BATON_PASS ]); game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); await game.phaseInterceptor.to(PostSummonPhase, false); // 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 74afc910faf..4f76563a78b 100644 --- a/src/test/moves/belly_drum.test.ts +++ b/src/test/moves/belly_drum.test.ts @@ -5,7 +5,7 @@ import { TurnEndPhase } from "#app/phases"; import { getMovePosition } from "#test/utils/gameManagerUtils"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; const TIMEOUT = 20 * 1000; // RATIO : HP Cost of Move @@ -39,7 +39,7 @@ 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", + 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]); @@ -50,47 +50,47 @@ 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", + 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 = Math.floor(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.doAttack(getMovePosition(game.scene, 0, 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", + 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.doAttack(getMovePosition(game.scene, 0, 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", + test("fails if the user's health is less than 1/2", async() => { await game.startBattle([Species.MAGIKARP]); @@ -102,7 +102,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/clangorous_soul.test.ts b/src/test/moves/clangorous_soul.test.ts index 5493466ba56..b6ddb93494a 100644 --- a/src/test/moves/clangorous_soul.test.ts +++ b/src/test/moves/clangorous_soul.test.ts @@ -1,11 +1,11 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import Phaser from "phaser"; import GameManager from "#test/utils/gameManager"; import { TurnEndPhase } from "#app/phases"; import { getMovePosition } from "#test/utils/gameManagerUtils"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { SPLASH_ONLY } from "#test/utils/testUtils"; const TIMEOUT = 20 * 1000; @@ -14,7 +14,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,75 +40,75 @@ 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", + 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 = Math.floor(leadPokemon.getMaxHp() / RATIO); - - game.doAttack(getMovePosition(game.scene, 0, 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); - }, TIMEOUT - ); - - test("Clangorous Soul will still take effect if one or more of the involved stats are not at max", - async() => { await game.startBattle([Species.MAGIKARP]); const leadPokemon = game.scene.getPlayerPokemon()!; 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; - game.doAttack(getMovePosition(game.scene, 0, 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(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 fails if all stats involved are at max", + 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 = Math.floor(leadPokemon.getMaxHp() / RATIO); + + //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.doAttack(getMovePosition(game.scene, 0, Moves.CLANGOROUS_SOUL)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost); + 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 + ); + + 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.doAttack(getMovePosition(game.scene, 0, 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", + it("fails if the user's health is less than 1/3", async() => { await game.startBattle([Species.MAGIKARP]); @@ -120,11 +120,11 @@ describe("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/double_team.test.ts b/src/test/moves/double_team.test.ts index 2153b856517..dcbb9149b21 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.js"; +import { Stat } from "#enums/stat"; import { Abilities } from "#app/enums/abilities.js"; import { TurnEndPhase } from "#app/phases"; import GameManager from "#test/utils/gameManager"; @@ -33,20 +33,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.doAttack(getMovePosition(game.scene, 0, 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 6ec7521f678..23cbb82d90f 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.js"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; @@ -64,9 +64,8 @@ describe("Moves - Dragon Rage", () => { game.doAttack(getMovePosition(game.scene, 0, 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 () => { @@ -75,20 +74,18 @@ describe("Moves - Dragon Rage", () => { game.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, 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 () => { @@ -97,9 +94,8 @@ describe("Moves - Dragon Rage", () => { game.doAttack(getMovePosition(game.scene, 0, 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 () => { @@ -107,20 +103,18 @@ describe("Moves - Dragon Rage", () => { game.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, 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 () => { @@ -129,8 +123,7 @@ describe("Moves - Dragon Rage", () => { game.doAttack(getMovePosition(game.scene, 0, 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 6965ced46d9..4805f929d01 100644 --- a/src/test/moves/fillet_away.test.ts +++ b/src/test/moves/fillet_away.test.ts @@ -5,7 +5,7 @@ import { TurnEndPhase } from "#app/phases"; import { getMovePosition } from "#test/utils/gameManagerUtils"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { BattleStat } from "#app/data/battle-stat"; +import { Stat } from "#enums/stat"; import { SPLASH_ONLY } from "#test/utils/testUtils"; const TIMEOUT = 20 * 1000; @@ -51,9 +51,9 @@ 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 ); @@ -64,17 +64,17 @@ describe("Moves - FILLET AWAY", () => { const leadPokemon = game.scene.getPlayerPokemon()!; const hpLost = Math.floor(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.doAttack(getMovePosition(game.scene, 0, 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 ); @@ -84,17 +84,17 @@ describe("Moves - FILLET AWAY", () => { 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.doAttack(getMovePosition(game.scene, 0, 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 ); @@ -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 979bc40646c..06693b0a986 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.js"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import { DamagePhase, TurnEndPhase } from "#app/phases"; @@ -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.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, Moves.FISSURE)); diff --git a/src/test/moves/flower_shield.test.ts b/src/test/moves/flower_shield.test.ts index 7ca5fb8bc62..a8bf4ff871d 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.js"; +import { Stat } from "#enums/stat"; import { SemiInvulnerableTag } from "#app/data/battler-tags.js"; import { Type } from "#app/data/type.js"; import { Biome } from "#app/enums/biome.js"; @@ -35,24 +35,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.doAttack(getMovePosition(game.scene, 0, 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]); @@ -61,21 +61,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.doAttack(getMovePosition(game.scene, 0, Moves.FLOWER_SHIELD)); game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); 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); @@ -84,32 +84,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.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, 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 420dd7e0762..cf207c9bc77 100644 --- a/src/test/moves/follow_me.test.ts +++ b/src/test/moves/follow_me.test.ts @@ -86,7 +86,7 @@ describe("Moves - Follow Me", () => { game.doAttack(getMovePosition(game.scene, 1, Moves.FOLLOW_ME)); 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 3ccd31bd29e..fc6635ee4f7 100644 --- a/src/test/moves/freezy_frost.test.ts +++ b/src/test/moves/freezy_frost.test.ts @@ -1,5 +1,5 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { MoveEndPhase, TurnInitPhase } from "#app/phases"; +import { Stat } from "#enums/stat"; +import { TurnInitPhase } from "#app/phases"; import GameManager from "#test/utils/gameManager"; import { getMovePosition } from "#test/utils/gameManagerUtils"; import { Abilities } from "#enums/abilities"; @@ -11,72 +11,52 @@ import { SPLASH_ONLY } from "#test/utils/testUtils"; import { allMoves } from "#app/data/move.js"; 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.doAttack(getMovePosition(game.scene, 0, Moves.SWORDS_DANCE)); - await game.phaseInterceptor.to(TurnInitPhase); + expect(user.getStatStage(Stat.ATK)).toBe(0); + expect(enemy.getStatStage(Stat.ATK)).toBe(0); - game.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, Moves.SWORDS_DANCE)); + await game.phaseInterceptor.to(TurnInitPhase); - game.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, Moves.SWORDS_DANCE)); - await game.phaseInterceptor.to(TurnInitPhase); - - const userAtkBefore = user.summonData.battleStats[BattleStat.ATK]; - expect(userAtkBefore).toBe(2); - - game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); - await game.phaseInterceptor.to(MoveEndPhase); - expect(user.summonData.battleStats[BattleStat.ATK]).toBe(0); - }); + game.doAttack(getMovePosition(game.scene, 0, 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/growth.test.ts b/src/test/moves/growth.test.ts index bfa3cc54896..06a98f28e92 100644 --- a/src/test/moves/growth.test.ts +++ b/src/test/moves/growth.test.ts @@ -1,16 +1,13 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { Stat } from "#app/data/pokemon-stat"; -import { CommandPhase, EnemyCommandPhase, TurnInitPhase } from "#app/phases"; +import { Stat } from "#enums/stat"; +import { EnemyCommandPhase, TurnInitPhase } from "#app/phases"; import GameManager from "#test/utils/gameManager"; import { getMovePosition } from "#test/utils/gameManagerUtils"; -import { Command } from "#app/ui/command-ui-handler"; -import { Mode } from "#app/ui/ui"; 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 } from "vitest"; - +import { SPLASH_ONLY } from "../utils/testUtils"; describe("Moves - Growth", () => { let phaserGame: Phaser.Game; @@ -28,37 +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.onNextPrompt("CommandPhase", Mode.COMMAND, () => { - game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); - }); - game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { - const movePosition = getMovePosition(game.scene, 0, moveToUse); - (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); - }); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(0); + + game.doAttack(getMovePosition(game.scene, 0, 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/haze.test.ts b/src/test/moves/haze.test.ts index 092575b8000..b0e1ddccf88 100644 --- a/src/test/moves/haze.test.ts +++ b/src/test/moves/haze.test.ts @@ -1,5 +1,5 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { MoveEndPhase, TurnInitPhase } from "#app/phases"; +import { Stat } from "#enums/stat"; +import { TurnInitPhase } from "#app/phases"; import GameManager from "#test/utils/gameManager"; import { getMovePosition } from "#test/utils/gameManagerUtils"; import { Abilities } from "#enums/abilities"; @@ -37,44 +37,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.doAttack(getMovePosition(game.scene, 0, Moves.SWORDS_DANCE)); await game.phaseInterceptor.to(TurnInitPhase); game.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, Moves.SWORDS_DANCE)); - await game.phaseInterceptor.to(TurnInitPhase); - - const userAtkBefore = user.summonData.battleStats[BattleStat.ATK]; - expect(userAtkBefore).toBe(2); - - game.doAttack(getMovePosition(game.scene, 0, 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/make_it_rain.test.ts b/src/test/moves/make_it_rain.test.ts index a4440401c4b..aa0a8616c01 100644 --- a/src/test/moves/make_it_rain.test.ts +++ b/src/test/moves/make_it_rain.test.ts @@ -1,4 +1,4 @@ -import { BattleStat } from "#app/data/battle-stat.js"; +import { Stat } from "#enums/stat"; import { MoveEndPhase, StatChangePhase } from "#app/phases"; import GameManager from "#test/utils/gameManager"; import { getMovePosition } from "#test/utils/gameManagerUtils"; @@ -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.doAttack(getMovePosition(game.scene, 0, Moves.MAKE_IT_RAIN)); game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); 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 () => { @@ -63,7 +63,7 @@ describe("Moves - Make It Rain", () => { await game.phaseInterceptor.to(StatChangePhase); 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,7 +71,7 @@ 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.doAttack(getMovePosition(game.scene, 0, Moves.MAKE_IT_RAIN)); @@ -80,13 +80,13 @@ describe("Moves - Make It Rain", () => { await game.phaseInterceptor.to(StatChangePhase); 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.doAttack(getMovePosition(game.scene, 0, Moves.MAKE_IT_RAIN)); game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); @@ -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/octolock.test.ts b/src/test/moves/octolock.test.ts index 8988109f431..140536b4aad 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.js"; import { CommandPhase, MoveEndPhase, TurnInitPhase } from "#app/phases"; import GameManager from "#test/utils/gameManager"; @@ -39,40 +39,40 @@ describe("Moves - Octolock", () => { game.override.ability(Abilities.BALL_FETCH); }); - it("Reduces DEf and SPDEF by 1 each turn", { timeout: 10000 }, async () => { + it("lowers DEF and SPDEF stat stages of the target Pokemon by 1 each turn", { timeout: 10000 }, async () => { await game.startBattle([Species.GRAPPLOCT]); - const enemyPokemon = game.scene.getEnemyField(); + const enemyPokemon = game.scene.getEnemyPokemon()!; // use Octolock and advance to init phase of next turn to check for stat changes game.doAttack(getMovePosition(game.scene, 0, 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); + 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.doAttack(getMovePosition(game.scene, 0, 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); + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-2); + expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-2); }); - it("Traps the target pokemon", { timeout: 10000 }, async () => { + it("traps the target Pokemon", { timeout: 10000 }, async () => { await game.startBattle([Species.GRAPPLOCT]); - const enemyPokemon = game.scene.getEnemyField(); + const enemyPokemon = game.scene.getEnemyPokemon()!; // before Octolock - enemy should not be trapped - expect(enemyPokemon[0].findTag(t => t instanceof TrappedTag)).toBeUndefined(); + expect(enemyPokemon.findTag(t => t instanceof TrappedTag)).toBeUndefined(); game.doAttack(getMovePosition(game.scene, 0, Moves.OCTOLOCK)); // after Octolock - enemy should be trapped await game.phaseInterceptor.to(MoveEndPhase); - expect(enemyPokemon[0].findTag(t => t instanceof TrappedTag)).toBeDefined(); + expect(enemyPokemon.findTag(t => t instanceof TrappedTag)).toBeDefined(); }); }); }); diff --git a/src/test/moves/spit_up.test.ts b/src/test/moves/spit_up.test.ts index ec0a53028ff..794a8593e13 100644 --- a/src/test/moves/spit_up.test.ts +++ b/src/test/moves/spit_up.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.js"; import { allMoves } from "#app/data/move.js"; import { BattlerTagType } from "#app/enums/battler-tag-type.js"; @@ -16,6 +16,8 @@ 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 }); }); @@ -34,8 +36,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([ spitUp.id, spitUp.id, spitUp.id, spitUp.id ]); game.override.ability(Abilities.NONE); + + vi.spyOn(spitUp, "calculateBattlePower"); }); describe("consumes all stockpile stacks to deal damage (scaling with stacks)", () => { @@ -52,13 +56,11 @@ describe("Moves - Spit Up", () => { expect(stockpilingTag).toBeDefined(); expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup); - vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower"); - game.doAttack(0); 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(); }); @@ -77,13 +79,11 @@ describe("Moves - Spit Up", () => { expect(stockpilingTag).toBeDefined(); expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup); - vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower"); - game.doAttack(0); 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(); }); @@ -103,13 +103,11 @@ describe("Moves - Spit Up", () => { expect(stockpilingTag).toBeDefined(); expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup); - vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower"); - game.doAttack(0); 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(); }); @@ -123,14 +121,12 @@ describe("Moves - Spit Up", () => { const stockpilingTag = pokemon.getTag(StockpilingTag)!; expect(stockpilingTag).toBeUndefined(); - vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower"); - game.doAttack(0); 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", () => { @@ -143,22 +139,20 @@ describe("Moves - Spit Up", () => { const stockpilingTag = pokemon.getTag(StockpilingTag)!; expect(stockpilingTag).toBeDefined(); - vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower"); - game.doAttack(0); 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(); }); @@ -174,26 +168,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.doAttack(0); 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 0893ba975d7..dcc805dbfb7 100644 --- a/src/test/moves/spotlight.test.ts +++ b/src/test/moves/spotlight.test.ts @@ -84,7 +84,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 375eeab3c95..6eb14f882c3 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.js"; import { MoveResult, TurnMove } from "#app/field/pokemon.js"; import { CommandPhase, TurnInitPhase } from "#app/phases"; @@ -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.doAttack(getMovePosition(game.scene, 0, 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 aed30445fd2..dd3360d2698 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.js"; import { BattlerTagType } from "#app/enums/battler-tag-type.js"; import { MoveResult, TurnMove } from "#app/field/pokemon.js"; @@ -137,7 +137,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]); @@ -150,20 +150,20 @@ describe("Moves - Swallow", () => { game.doAttack(0); 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()!; @@ -174,22 +174,17 @@ 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.doAttack(0); 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/tail_whip.test.ts b/src/test/moves/tail_whip.test.ts index 7630b31f7de..bb751cdbc69 100644 --- a/src/test/moves/tail_whip.test.ts +++ b/src/test/moves/tail_whip.test.ts @@ -1,14 +1,13 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { CommandPhase, EnemyCommandPhase, TurnInitPhase } from "#app/phases"; +import { Stat } from "#enums/stat"; +import { EnemyCommandPhase, TurnInitPhase } from "#app/phases"; import GameManager from "#test/utils/gameManager"; import { getMovePosition } from "#test/utils/gameManagerUtils"; -import { Command } from "#app/ui/command-ui-handler"; -import { Mode } from "#app/ui/ui"; 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 } from "vitest"; +import { SPLASH_ONLY } from "../utils/testUtils"; describe("Moves - Tail whip", () => { @@ -33,29 +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.onNextPrompt("CommandPhase", Mode.COMMAND, () => { - game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); - }); - game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { - const movePosition = getMovePosition(game.scene, 0, moveToUse); - (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); - }); + game.doAttack(getMovePosition(game.scene, 0, 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 b2643dc68f3..55e2b70d9d9 100644 --- a/src/test/moves/tailwind.test.ts +++ b/src/test/moves/tailwind.test.ts @@ -39,16 +39,16 @@ describe("Abilities - Wind Rider", () => { 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.doAttack(getMovePosition(game.scene, 0, Moves.TAILWIND)); game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); 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(); }); @@ -87,8 +87,8 @@ describe("Abilities - Wind Rider", () => { 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(); @@ -96,8 +96,8 @@ describe("Abilities - Wind Rider", () => { 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/tidy_up.test.ts b/src/test/moves/tidy_up.test.ts index e35a438c562..8a23d610a5f 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.js"; +import { Stat } from "#enums/stat"; import { ArenaTagType } from "#app/enums/arena-tag-type.js"; import { MoveEndPhase, TurnEndPhase } from "#app/phases"; import GameManager from "#test/utils/gameManager"; @@ -60,7 +60,6 @@ describe("Moves - Tidy Up", () => { game.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, 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.doAttack(getMovePosition(game.scene, 0, 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/ui/battle-info.ts b/src/ui/battle-info.ts index ceee07eda01..d27d44802ea 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 statOrderPlayer = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA, Stat.SPD ]; + private 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 { @@ -653,9 +653,9 @@ export default class BattleInfo extends Phaser.GameObjects.Container { const battleStats = pokemon.getStatStages(); const battleStatsStr = battleStats.join(""); - if (this.lastBattleStats !== battleStatsStr) { - this.updateBattleStats(battleStats); - this.lastBattleStats = battleStatsStr; + if (this.lastStats !== battleStatsStr) { + this.updateStats(battleStats); + this.lastStats = battleStatsStr; } this.shinyIcon.setVisible(pokemon.isShiny()); @@ -767,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].toString()); } }); } diff --git a/src/ui/battle-message-ui-handler.ts b/src/ui/battle-message-ui-handler.ts index 1c7dfb27630..88efaa38988 100644 --- a/src/ui/battle-message-ui-handler.ts +++ b/src/ui/battle-message-ui-handler.ts @@ -3,11 +3,11 @@ import { addBBCodeTextObject, addTextObject, getTextColor, TextStyle } from "./t 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, getStatKey, PERMANENT_STATS } from "#app/enums/stat.js"; export default class BattleMessageUiHandler extends MessageUiHandler { private levelUpStatsContainer: Phaser.GameObjects.Container; @@ -98,9 +98,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 += `${getStatKey(s)}\n`; } levelUpStatsLabelsContent.text = levelUpStatsLabelText; levelUpStatsLabelsContent.x -= levelUpStatsLabelsContent.displayWidth; diff --git a/src/ui/stats-container.ts b/src/ui/stats-container.ts index b4e799bafc0..5aab5268c86 100644 --- a/src/ui/stats-container.ts +++ b/src/ui/stats-container.ts @@ -1,7 +1,7 @@ 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 { getStatKey, PERMANENT_STATS } from "#app/enums/stat.js"; 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 +53,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], 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 030e67fcc30..eb4a14ad81a 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"; @@ -23,6 +22,7 @@ import { Ability } from "../data/ability.js"; import i18next from "i18next"; import {modifierSortFunc} from "../modifier/modifier"; import { PlayerGender } from "#enums/player-gender"; +import { Stat, getStatKey, PERMANENT_STATS } from "#app/enums/stat.js"; enum Page { PROFILE, @@ -838,10 +838,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 = getStatKey(stat); const rowIndex = s % 3; const colIndex = Math.floor(s / 3); @@ -852,7 +850,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);