Finalize BattleStat Removal

This commit is contained in:
xsn34kzx 2024-08-17 22:41:56 -04:00
parent 1f130f85a7
commit 2edb67b56d
36 changed files with 170 additions and 542 deletions

View File

@ -191,15 +191,15 @@ Now that the enemy Pokémon with the best matchup score is on the field (assumin
We then need to apply a 2x multiplier for the move's type effectiveness and a 1.5x multiplier since STAB applies. After applying these multipliers, the final score for this move is **75**. We then need to apply a 2x multiplier for the move's type effectiveness and a 1.5x multiplier since STAB applies. After applying these multipliers, the final score for this move is **75**.
- **Swords Dance**: As a non-attacking move, this move's benefit score is derived entirely from the sum of its attributes' benefit scores. Swords Dance's `StatChangeAttr` has a user benefit score of 0 and a target benefit score that, in this case, simplifies to - **Swords Dance**: As a non-attacking move, this move's benefit score is derived entirely from the sum of its attributes' benefit scores. Swords Dance's `StatStageChangeAttr` has a user benefit score of 0 and a target benefit score that, in this case, simplifies to
$\text{TBS}=4\times \text{levels} + (-2\times \text{sign(levels)})$ $\text{TBS}=4\times \text{levels} + (-2\times \text{sign(levels)})$
where `levels` is the number of stat stages added by the attribute (in this case, +2). The final score for this move is **6** (Note: because this move is self-targeted, we don't flip the sign of TBS when computing the target score). where `levels` is the number of stat stages added by the attribute (in this case, +2). The final score for this move is **6** (Note: because this move is self-targeted, we don't flip the sign of TBS when computing the target score).
- **Crush Claw**: This move is a 75-power Normal-type physical attack with a 50 percent chance to lower the target's Defense by one stage. The additional effect is implemented by the same `StatChangeAttr` as Swords Dance, so we can use the same formulas from before to compute the total TBS and base target score. - **Crush Claw**: This move is a 75-power Normal-type physical attack with a 50 percent chance to lower the target's Defense by one stage. The additional effect is implemented by the same `StatStageChangeAttr` as Swords Dance, so we can use the same formulas from before to compute the total TBS and base target score.
$\text{TBS}=\text{getTargetBenefitScore(StatChangeAttr)}-\text{attackScore}$ $\text{TBS}=\text{getTargetBenefitScore(StatStageChangeAttr)}-\text{attackScore}$
$\text{TBS}=(-4 + 2)-(-2\times 2 + \lfloor \frac{75}{5} \rfloor)=-2-11=-13$ $\text{TBS}=(-4 + 2)-(-2\times 2 + \lfloor \frac{75}{5} \rfloor)=-2-11=-13$
@ -221,4 +221,4 @@ When implementing a new move attribute, it's important to override `MoveAttr`'s
- A move's **user benefit score (UBS)** incentivizes (or discourages) the move's usage in general. A positive UBS gives the move more incentive to be used, while a negative UBS gives the move less incentive. - A move's **user benefit score (UBS)** incentivizes (or discourages) the move's usage in general. A positive UBS gives the move more incentive to be used, while a negative UBS gives the move less incentive.
- A move's **target benefit score (TBS)** incentivizes (or discourages) the move's usage on a specific target. A positive TBS indicates the move is better used on the user or its allies, while a negative TBS indicates the move is better used on enemies. - A move's **target benefit score (TBS)** incentivizes (or discourages) the move's usage on a specific target. A positive TBS indicates the move is better used on the user or its allies, while a negative TBS indicates the move is better used on enemies.
- **The total benefit score (UBS + TBS) of a move should never be 0.** The move selection algorithm assumes the move's benefit score is unimplemented if the total score is 0 and penalizes the move's usage as a result. With status moves especially, it's important to have some form of implementation among the move's attributes to avoid this scenario. - **The total benefit score (UBS + TBS) of a move should never be 0.** The move selection algorithm assumes the move's benefit score is unimplemented if the total score is 0 and penalizes the move's usage as a result. With status moves especially, it's important to have some form of implementation among the move's attributes to avoid this scenario.
- **Score functions that use formulas should include comments.** If your attribute requires complex logic or formulas to calculate benefit scores, please add comments to explain how the logic works and its intended effect on the enemy's decision making. - **Score functions that use formulas should include comments.** If your attribute requires complex logic or formulas to calculate benefit scores, please add comments to explain how the logic works and its intended effect on the enemy's decision making.

View File

@ -1821,7 +1821,7 @@ export class CopyFaintedAllyAbilityAbAttr extends PostKnockOutAbAttr {
} }
} }
export class IgnoreOpponentStatStageChangesAbAttr extends AbAttr { export class IgnoreOpponentStatStagesAbAttr extends AbAttr {
constructor() { constructor() {
super(false); super(false);
} }
@ -2010,8 +2010,7 @@ export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr {
applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled); applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled);
} }
if (!cancelled.value) { if (!cancelled.value) {
const statChangePhase = new StatStageChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.stages); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.stages));
pokemon.scene.unshiftPhase(statChangePhase);
} }
} }
return true; return true;
@ -4690,7 +4689,7 @@ export function initAbilities() {
new Ability(Abilities.FOREWARN, 4) new Ability(Abilities.FOREWARN, 4)
.attr(ForewarnAbAttr), .attr(ForewarnAbAttr),
new Ability(Abilities.UNAWARE, 4) new Ability(Abilities.UNAWARE, 4)
.attr(IgnoreOpponentStatStageChangesAbAttr) .attr(IgnoreOpponentStatStagesAbAttr)
.ignorable(), .ignorable(),
new Ability(Abilities.TINTED_LENS, 4) new Ability(Abilities.TINTED_LENS, 4)
//@ts-ignore //@ts-ignore

View File

@ -4,7 +4,7 @@ import * as Utils from "../utils";
import { MoveCategory, allMoves, MoveTarget } from "./move"; import { MoveCategory, allMoves, MoveTarget } from "./move";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { HitResult, PokemonMove } from "../field/pokemon"; import Pokemon, { HitResult, PokemonMove } from "../field/pokemon";
import { MoveEffectPhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases"; import { MoveEffectPhase, PokemonHealPhase, ShowAbilityPhase, StatStageChangePhase } from "../phases";
import { StatusEffect } from "./status-effect"; import { StatusEffect } from "./status-effect";
import { BattlerIndex } from "../battle"; import { BattlerIndex } from "../battle";
import { BlockNonDirectDamageAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability"; import { BlockNonDirectDamageAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability";
@ -725,8 +725,8 @@ class StickyWebTag extends ArenaTrapTag {
applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled); applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled);
if (!cancelled.value) { if (!cancelled.value) {
pokemon.scene.queueMessage(i18next.t("arenaTag:stickyWebActivateTrap", { pokemonName: pokemon.getNameToRender() })); pokemon.scene.queueMessage(i18next.t("arenaTag:stickyWebActivateTrap", { pokemonName: pokemon.getNameToRender() }));
const statLevels = new Utils.NumberHolder(-1); const stages = new Utils.NumberHolder(-1);
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.SPD ], statLevels.value)); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value));
} }
} }
@ -814,7 +814,7 @@ class TailwindTag extends ArenaTag {
// Raise attack by one stage if party member has WIND_RIDER ability // Raise attack by one stage if party member has WIND_RIDER ability
if (pokemon.hasAbility(Abilities.WIND_RIDER)) { if (pokemon.hasAbility(Abilities.WIND_RIDER)) {
pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.getBattlerIndex())); pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.getBattlerIndex()));
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.ATK ], 1, true)); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.ATK ], 1, true));
} }
} }
} }

View File

@ -1,5 +1,5 @@
import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "./battle-anims"; import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "./battle-anims";
import { CommonAnimPhase, MoveEffectPhase, MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangeCallback, StatStageChangePhase } from "../phases"; import { CommonAnimPhase, MoveEffectPhase, MovePhase, PokemonHealPhase, ShowAbilityPhase, StatStageChangeCallback, StatStageChangePhase } from "../phases";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { MoveResult, HitResult } from "../field/pokemon"; import Pokemon, { MoveResult, HitResult } from "../field/pokemon";
import { StatusEffect } from "./status-effect"; import { StatusEffect } from "./status-effect";
@ -1629,7 +1629,7 @@ export class StockpilingTag extends BattlerTag {
super(BattlerTagType.STOCKPILING, BattlerTagLapseType.CUSTOM, 1, sourceMove); super(BattlerTagType.STOCKPILING, BattlerTagLapseType.CUSTOM, 1, sourceMove);
} }
private onStatsChanged: StatChangeCallback = (_, statsChanged, statChanges) => { private onStatStagesChanged: StatStageChangeCallback = (_, statsChanged, statChanges) => {
const defChange = statChanges[statsChanged.indexOf(Stat.DEF)] ?? 0; const defChange = statChanges[statsChanged.indexOf(Stat.DEF)] ?? 0;
const spDefChange = statChanges[statsChanged.indexOf(Stat.SPDEF)] ?? 0; const spDefChange = statChanges[statsChanged.indexOf(Stat.SPDEF)] ?? 0;
@ -1669,7 +1669,7 @@ export class StockpilingTag extends BattlerTag {
// Attempt to increase DEF and SPDEF by one stage, keeping track of successful changes. // Attempt to increase DEF and SPDEF by one stage, keeping track of successful changes.
pokemon.scene.unshiftPhase(new StatStageChangePhase( pokemon.scene.unshiftPhase(new StatStageChangePhase(
pokemon.scene, pokemon.getBattlerIndex(), true, pokemon.scene, pokemon.getBattlerIndex(), true,
[Stat.SPDEF, Stat.DEF], 1, true, false, true, this.onStatsChanged [Stat.SPDEF, Stat.DEF], 1, true, false, true, this.onStatStagesChanged
)); ));
} }
} }

View File

@ -1,4 +1,4 @@
import { PokemonHealPhase, StatChangePhase } from "../phases"; import { PokemonHealPhase, StatStageChangePhase } from "../phases";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { HitResult } from "../field/pokemon"; import Pokemon, { HitResult } from "../field/pokemon";
import { getStatusEffectHealText } from "./status-effect"; import { getStatusEffectHealText } from "./status-effect";
@ -99,7 +99,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
const stat: BattleStat = berryType - BerryType.ENIGMA; const stat: BattleStat = berryType - BerryType.ENIGMA;
const statStages = new Utils.NumberHolder(1); const statStages = new Utils.NumberHolder(1);
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, statStages); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, statStages);
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], statStages.value)); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], statStages.value));
}; };
case BerryType.LANSAT: case BerryType.LANSAT:
return (pokemon: Pokemon) => { return (pokemon: Pokemon) => {
@ -114,9 +114,9 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
pokemon.battleData.berriesEaten.push(berryType); pokemon.battleData.berriesEaten.push(berryType);
} }
const randStat = Utils.randSeedInt(Stat.EVA, Stat.ATK); const randStat = Utils.randSeedInt(Stat.EVA, Stat.ATK);
const statLevels = new Utils.NumberHolder(2); const stages = new Utils.NumberHolder(2);
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, statLevels); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, stages);
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ randStat ], statLevels.value)); // TODO: BattleStats pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ randStat ], stages.value));
}; };
case BerryType.LEPPA: case BerryType.LEPPA:
return (pokemon: Pokemon) => { return (pokemon: Pokemon) => {

View File

@ -1066,7 +1066,7 @@ export class StatusMoveTypeImmunityAttr extends MoveAttr {
} }
} }
export class IgnoreOpponentStatChangesAttr extends MoveAttr { export class IgnoreOpponentStatStagesAttr extends MoveAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
(args[0] as Utils.IntegerHolder).value = 0; (args[0] as Utils.IntegerHolder).value = 0;
@ -2614,14 +2614,14 @@ export class StatStageChangeAttr extends MoveEffectAttr {
export class PostVictoryStatStageChangeAttr extends MoveAttr { export class PostVictoryStatStageChangeAttr extends MoveAttr {
private stats: BattleStat[]; private stats: BattleStat[];
private levels: integer; private stages: number;
private condition: MoveConditionFunc | null; private condition: MoveConditionFunc | null;
private showMessage: boolean; private showMessage: boolean;
constructor(stats: BattleStat[], levels: integer, selfTarget?: boolean, condition?: MoveConditionFunc, showMessage: boolean = true, firstHitOnly: boolean = false) { constructor(stats: BattleStat[], stages: number, selfTarget?: boolean, condition?: MoveConditionFunc, showMessage: boolean = true, firstHitOnly: boolean = false) {
super(); super();
this.stats = stats; this.stats = stats;
this.levels = levels; this.stages = stages;
this.condition = condition!; // TODO: is this bang correct? this.condition = condition!; // TODO: is this bang correct?
this.showMessage = showMessage; this.showMessage = showMessage;
} }
@ -2629,7 +2629,7 @@ export class PostVictoryStatStageChangeAttr extends MoveAttr {
if (this.condition && !this.condition(user, target, move)) { if (this.condition && !this.condition(user, target, move)) {
return; return;
} }
const statChangeAttr = new StatStageChangeAttr(this.stats, this.levels, this.showMessage); const statChangeAttr = new StatStageChangeAttr(this.stats, this.stages, this.showMessage);
statChangeAttr.apply(user, target, move); statChangeAttr.apply(user, target, move);
} }
} }
@ -3313,9 +3313,9 @@ export class HitCountPowerAttr extends VariablePowerAttr {
} }
/** /**
* Turning a once was (StatChangeCountPowerAttr) statement and making it available to call for any attribute. * Tallies the number of positive stages for a given {@linkcode Pokemon}.
* @param {Pokemon} pokemon The pokemon that is being used to calculate the count of positive stats * @param pokemon The {@linkcode Pokemon} that is being used to calculate the count of positive stats
* @returns {number} Returns the amount of positive stats * @returns the amount of positive stats
*/ */
const countPositiveStatStages = (pokemon: Pokemon): number => { const countPositiveStatStages = (pokemon: Pokemon): number => {
return pokemon.getStatStages().reduce((total, stat) => (stat && stat > 0) ? total + stat : total, 0); return pokemon.getStatStages().reduce((total, stat) => (stat && stat > 0) ? total + stat : total, 0);
@ -6666,7 +6666,7 @@ export function initMoves() {
.attr(ConfuseAttr), .attr(ConfuseAttr),
new SelfStatusMove(Moves.BELLY_DRUM, Type.NORMAL, -1, 10, -1, 0, 2) new SelfStatusMove(Moves.BELLY_DRUM, Type.NORMAL, -1, 10, -1, 0, 2)
.attr(CutHpStatStageBoostAttr, [ Stat.ATK ], 12, 2, (user) => { .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 user.scene.queueMessage(i18next.t("moveTriggers:cutOwnHpAndMaximizedStat", { pokemonName: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) }));
}), }),
new AttackMove(Moves.SLUDGE_BOMB, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 30, 0, 2) new AttackMove(Moves.SLUDGE_BOMB, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 30, 0, 2)
.attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatusEffectAttr, StatusEffect.POISON)
@ -7544,7 +7544,7 @@ export function initMoves() {
.attr(ConsecutiveUseMultiBasePowerAttr, 5, false) .attr(ConsecutiveUseMultiBasePowerAttr, 5, false)
.soundBased(), .soundBased(),
new AttackMove(Moves.CHIP_AWAY, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 5) new AttackMove(Moves.CHIP_AWAY, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 5)
.attr(IgnoreOpponentStatChangesAttr), .attr(IgnoreOpponentStatStagesAttr),
new AttackMove(Moves.CLEAR_SMOG, Type.POISON, MoveCategory.SPECIAL, 50, -1, 15, -1, 0, 5) new AttackMove(Moves.CLEAR_SMOG, Type.POISON, MoveCategory.SPECIAL, 50, -1, 15, -1, 0, 5)
.attr(ResetStatsAttr, false), .attr(ResetStatsAttr, false),
new AttackMove(Moves.STORED_POWER, Type.PSYCHIC, MoveCategory.SPECIAL, 20, 100, 10, -1, 0, 5) new AttackMove(Moves.STORED_POWER, Type.PSYCHIC, MoveCategory.SPECIAL, 20, 100, 10, -1, 0, 5)
@ -7636,7 +7636,7 @@ export function initMoves() {
.attr(HitHealAttr) .attr(HitHealAttr)
.triageMove(), .triageMove(),
new AttackMove(Moves.SACRED_SWORD, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 5) new AttackMove(Moves.SACRED_SWORD, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 5)
.attr(IgnoreOpponentStatChangesAttr) .attr(IgnoreOpponentStatStagesAttr)
.slicingMove(), .slicingMove(),
new AttackMove(Moves.RAZOR_SHELL, Type.WATER, MoveCategory.PHYSICAL, 75, 95, 10, 50, 0, 5) new AttackMove(Moves.RAZOR_SHELL, Type.WATER, MoveCategory.PHYSICAL, 75, 95, 10, 50, 0, 5)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1) .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
@ -8028,7 +8028,7 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.DARKEST_LARIAT, Type.DARK, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 7) new AttackMove(Moves.DARKEST_LARIAT, Type.DARK, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 7)
.attr(IgnoreOpponentStatChangesAttr), .attr(IgnoreOpponentStatStagesAttr),
new AttackMove(Moves.SPARKLING_ARIA, Type.WATER, MoveCategory.SPECIAL, 90, 100, 10, 100, 0, 7) new AttackMove(Moves.SPARKLING_ARIA, Type.WATER, MoveCategory.SPECIAL, 90, 100, 10, 100, 0, 7)
.attr(HealStatusEffectAttr, false, StatusEffect.BURN) .attr(HealStatusEffectAttr, false, StatusEffect.BURN)
.soundBased() .soundBased()

View File

@ -1,7 +1,7 @@
import { Gender } from "./gender"; import { Gender } from "./gender";
import { PokeballType } from "./pokeball"; import { PokeballType } from "./pokeball";
import Pokemon from "../field/pokemon"; import Pokemon from "../field/pokemon";
import { Stat } from "./pokemon-stat"; import { Stat } from "#enums/stat";
import { Type } from "./type"; import { Type } from "./type";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { SpeciesFormKey } from "./pokemon-species"; import { SpeciesFormKey } from "./pokemon-species";

View File

@ -15,7 +15,7 @@ import { QuantizerCelebi, argbFromRgba, rgbaFromArgb } from "@material/material-
import { VariantSet } from "./variant"; import { VariantSet } from "./variant";
import i18next from "i18next"; import i18next from "i18next";
import { Localizable } from "#app/interfaces/locales"; import { Localizable } from "#app/interfaces/locales";
import { Stat } from "./pokemon-stat"; import { Stat } from "#enums/stat";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { PartyMemberStrength } from "#enums/party-member-strength"; import { PartyMemberStrength } from "#enums/party-member-strength";
import { Species } from "#enums/species"; import { Species } from "#enums/species";

View File

@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "../battle-scene";
import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
import { variantData } from "#app/data/variant"; import { variantData } from "#app/data/variant";
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info"; 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, StatStageChangeAttr, 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, IgnoreOpponentStatStagesAttr, 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 { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species";
import { Constructor } from "#app/utils"; import { Constructor } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
@ -21,7 +21,7 @@ import { DamagePhase, FaintPhase, LearnMovePhase, MoveEffectPhase, ObtainStatusE
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag } from "../data/battler-tags"; import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag } from "../data/battler-tags";
import { WeatherType } from "../data/weather"; import { WeatherType } from "../data/weather";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
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 { Ability, AbAttr, StatStageMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, 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 PokemonData from "../system/pokemon-data";
import { BattlerIndex } from "../battle"; import { BattlerIndex } from "../battle";
import { Mode } from "../ui/ui"; import { Mode } from "../ui/ui";
@ -681,7 +681,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @returns the numeric value of the desired {@linkcode Stat} * @returns the numeric value of the desired {@linkcode Stat}
*/ */
getStat(stat: PermanentStat, ignoreOverride: boolean = true): number { getStat(stat: PermanentStat, ignoreOverride: boolean = true): number {
if (!ignoreOverride && (this.summonData?.stats[stat] !== 0)) { if (!ignoreOverride && this.summonData && (this.summonData.stats[stat] !== 0)) {
return this.summonData.stats[stat]; return this.summonData.stats[stat];
} }
return this.stats[stat]; return this.stats[stat];
@ -725,13 +725,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** /**
* Writes the value to the in-battle stage of the corresponding {@linkcode BattleStat} of the {@linkcode Pokemon}. * Writes the value to the in-battle stage of the corresponding {@linkcode BattleStat} of the {@linkcode Pokemon}.
* *
* Note that this does nothing if {@linkcode value} is less than -6 and greater than 6. * Note that, if the value is not within a range of [-6, 6], it will be forced to the closest range bound.
* @param stat the {@linkcode BattleStat} whose stage is to be overwritten * @param stat the {@linkcode BattleStat} whose stage is to be overwritten
* @param value the desired numeric value * @param value the desired numeric value
*/ */
setStatStage(stat: BattleStat, value: number): void { setStatStage(stat: BattleStat, value: number): void {
if ((value >= -6) && (value >= 6) && this.summonData) { if (this.summonData) {
this.summonData.statStages[stat - 1] = value; if (value >= -6) {
this.summonData.statStages[stat - 1] = Math.min(value, 6);
} else {
this.summonData.statStages[stat - 1] = Math.max(value, -6);
}
} }
} }
@ -750,9 +754,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
break; break;
} }
} }
applyAbAttrs(IgnoreOpponentStatStageChangesAbAttr, opponent, null, statLevel); applyAbAttrs(IgnoreOpponentStatStagesAbAttr, opponent, null, statLevel);
if (move) { if (move) {
applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, opponent, move, statLevel); applyMoveAttrs(IgnoreOpponentStatStagesAttr, this, opponent, move, statLevel);
} }
} }
if (this.isPlayer()) { if (this.isPlayer()) {
@ -1950,10 +1954,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const userAccStage = new Utils.IntegerHolder(this.getStatStage(Stat.ACC)); const userAccStage = new Utils.IntegerHolder(this.getStatStage(Stat.ACC));
const targetEvaStage = new Utils.IntegerHolder(target.getStatStage(Stat.EVA)); const targetEvaStage = new Utils.IntegerHolder(target.getStatStage(Stat.EVA));
applyAbAttrs(IgnoreOpponentStatStageChangesAbAttr, target, null, userAccStage); applyAbAttrs(IgnoreOpponentStatStagesAbAttr, target, null, userAccStage);
applyAbAttrs(IgnoreOpponentStatStageChangesAbAttr, this, null, targetEvaStage); applyAbAttrs(IgnoreOpponentStatStagesAbAttr, this, null, targetEvaStage);
applyAbAttrs(IgnoreOpponentEvasionAbAttr, this, null, targetEvaStage); applyAbAttrs(IgnoreOpponentEvasionAbAttr, this, null, targetEvaStage);
applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, target, sourceMove, targetEvaStage); applyMoveAttrs(IgnoreOpponentStatStagesAttr, this, target, sourceMove, targetEvaStage);
this.scene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), Stat.ACC, userAccStage); this.scene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), Stat.ACC, userAccStage);
if (target.findTag(t => t instanceof ExposedTag)) { if (target.findTag(t => t instanceof ExposedTag)) {
@ -1967,10 +1971,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
: 3 / (3 + Math.min(targetEvaStage.value - userAccStage.value, 6)); : 3 / (3 + Math.min(targetEvaStage.value - userAccStage.value, 6));
} }
applyStatStageMultiplierAbAttrs(StatStageMultiplierAbAttr, this, Stat.ACC, accuracyMultiplier, sourceMove); // TODO: BattleStat applyStatStageMultiplierAbAttrs(StatStageMultiplierAbAttr, this, Stat.ACC, accuracyMultiplier, sourceMove);
const evasionMultiplier = new Utils.NumberHolder(1); const evasionMultiplier = new Utils.NumberHolder(1);
applyStatStageMultiplierAbAttrs(StatStageMultiplierAbAttr, target, Stat.EVA, evasionMultiplier); // TODO: BattleStat applyStatStageMultiplierAbAttrs(StatStageMultiplierAbAttr, target, Stat.EVA, evasionMultiplier);
accuracyMultiplier.value /= evasionMultiplier.value; accuracyMultiplier.value /= evasionMultiplier.value;

View File

@ -4,7 +4,7 @@ import BattleScene from "../battle-scene";
import { getLevelTotalExp } from "../data/exp"; import { getLevelTotalExp } from "../data/exp";
import { MAX_PER_TYPE_POKEBALLS, PokeballType } from "../data/pokeball"; import { MAX_PER_TYPE_POKEBALLS, PokeballType } from "../data/pokeball";
import Pokemon, { PlayerPokemon } from "../field/pokemon"; import Pokemon, { PlayerPokemon } from "../field/pokemon";
import { Stat } from "../data/pokemon-stat"; import { Stat } from "#enums/stat";
import { addTextObject, TextStyle } from "../ui/text"; import { addTextObject, TextStyle } from "../ui/text";
import { Type } from "../data/type"; import { Type } from "../data/type";
import { EvolutionPhase } from "../evolution-phase"; import { EvolutionPhase } from "../evolution-phase";

View File

@ -3446,19 +3446,19 @@ export class ShowAbilityPhase extends PokemonPhase {
} }
} }
export type StatChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void; export type StatStageChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void;
export class StatStageChangePhase extends PokemonPhase { // TODO: BattleStat export class StatStageChangePhase extends PokemonPhase {
private stats: BattleStat[]; private stats: BattleStat[];
private selfTarget: boolean; private selfTarget: boolean;
private stages: integer; private stages: integer;
private showMessage: boolean; private showMessage: boolean;
private ignoreAbilities: boolean; private ignoreAbilities: boolean;
private canBeCopied: boolean; private canBeCopied: boolean;
private onChange: StatChangeCallback | null; private onChange: StatStageChangeCallback | null;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], stages: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatChangeCallback | null = null) { constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], stages: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatStageChangeCallback | null = null) {
super(scene, battlerIndex); super(scene, battlerIndex);
this.selfTarget = selfTarget; this.selfTarget = selfTarget;
@ -3473,20 +3473,11 @@ export class StatStageChangePhase extends PokemonPhase { // TODO: BattleStat
start() { start() {
const pokemon = this.getPokemon(); const pokemon = this.getPokemon();
let random = false;
if (this.stats.length === 1 && this.stats[0] === BattleStat.RAND) {
this.stats[0] = this.getRandomStat();
random = true;
}
this.aggregateStatChanges(random);
if (!pokemon.isActive(true)) { if (!pokemon.isActive(true)) {
return this.end(); return this.end();
} }
const filteredStats = this.stats.map(s => s !== BattleStat.RAND ? s : this.getRandomStat()).filter(stat => { const filteredStats = this.stats.filter(stat => {
const cancelled = new Utils.BooleanHolder(false); const cancelled = new Utils.BooleanHolder(false);
if (!this.selfTarget && this.stages < 0) { if (!this.selfTarget && this.stages < 0) {
@ -3494,42 +3485,41 @@ export class StatStageChangePhase extends PokemonPhase { // TODO: BattleStat
} }
if (!cancelled.value && !this.selfTarget && this.stages < 0) { if (!cancelled.value && !this.selfTarget && this.stages < 0) {
applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, this.getPokemon(), stat, cancelled); applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled);
} }
return !cancelled.value; return !cancelled.value;
}); });
const levels = new Utils.IntegerHolder(this.stages); const stages = new Utils.IntegerHolder(this.stages);
if (!this.ignoreAbilities) { if (!this.ignoreAbilities) {
applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, levels); applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, stages);
} }
const battleStats = this.getPokemon().summonData.battleStats; const relLevels = filteredStats.map(s => (stages.value >= 1 ? Math.min(pokemon.getStatStage(s) + stages.value, 6) : Math.max(pokemon.getStatStage(s) + stages.value, -6)) - pokemon.getStatStage(s));
const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats![stat] + levels.value, 6) : Math.max(battleStats![stat] + levels.value, -6)) - battleStats![stat]);
this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels); this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels);
const end = () => { const end = () => {
if (this.showMessage) { if (this.showMessage) {
const messages = this.getStatChangeMessages(filteredStats, levels.value, relLevels); const messages = this.getStatStageChangeMessages(filteredStats, stages.value, relLevels);
for (const message of messages) { for (const message of messages) {
this.scene.queueMessage(message); this.scene.queueMessage(message);
} }
} }
for (const stat of filteredStats) { for (const s of filteredStats) {
pokemon.summonData.battleStats[stat] = Math.max(Math.min(pokemon.summonData.battleStats[stat] + levels.value, 6), -6); pokemon.setStatStage(s, pokemon.getStatStage(s) + stages.value);
} }
if (levels.value > 0 && this.canBeCopied) { if (stages.value > 0 && this.canBeCopied) {
for (const opponent of pokemon.getOpponents()) { for (const opponent of pokemon.getOpponents()) {
applyAbAttrs(StatStageChangeCopyAbAttr, opponent, null, this.stats, levels.value); applyAbAttrs(StatStageChangeCopyAbAttr, opponent, null, this.stats, stages.value);
} }
} }
applyPostStatStageChangeAbAttrs(PostStatStageChangeAbAttr, pokemon, filteredStats, this.levels, this.selfTarget); applyPostStatStageChangeAbAttrs(PostStatStageChangeAbAttr, pokemon, filteredStats, this.stages, this.selfTarget);
// Look for any other stat change phases; if this is the last one, do White Herb check // Look for any other stat change phases; if this is the last one, do White Herb check
const existingPhase = this.scene.findPhase(p => p instanceof StatStageChangePhase && p.battlerIndex === this.battlerIndex); const existingPhase = this.scene.findPhase(p => p instanceof StatStageChangePhase && p.battlerIndex === this.battlerIndex);
@ -3556,20 +3546,20 @@ export class StatStageChangePhase extends PokemonPhase { // TODO: BattleStat
const pokemonMaskSprite = pokemon.maskSprite; const pokemonMaskSprite = pokemon.maskSprite;
const tileX = (this.player ? 106 : 236) * pokemon.getSpriteScale() * this.scene.field.scale; const tileX = (this.player ? 106 : 236) * pokemon.getSpriteScale() * this.scene.field.scale;
const tileY = ((this.player ? 148 : 84) + (levels.value >= 1 ? 160 : 0)) * pokemon.getSpriteScale() * this.scene.field.scale; const tileY = ((this.player ? 148 : 84) + (stages.value >= 1 ? 160 : 0)) * pokemon.getSpriteScale() * this.scene.field.scale;
const tileWidth = 156 * this.scene.field.scale * pokemon.getSpriteScale(); const tileWidth = 156 * this.scene.field.scale * pokemon.getSpriteScale();
const tileHeight = 316 * this.scene.field.scale * pokemon.getSpriteScale(); const tileHeight = 316 * this.scene.field.scale * pokemon.getSpriteScale();
// On increase, show the red sprite located at ATK // On increase, show the red sprite located at ATK
// On decrease, show the blue sprite located at SPD // On decrease, show the blue sprite located at SPD
const spriteColor = levels.value >= 1 ? Stat[Stat.ATK].toLowerCase() : Stat[Stat.SPD].toLowerCase(); const spriteColor = stages.value >= 1 ? Stat[Stat.ATK].toLowerCase() : Stat[Stat.SPD].toLowerCase();
const statSprite = this.scene.add.tileSprite(tileX, tileY, tileWidth, tileHeight, "battle_stats", spriteColor); const statSprite = this.scene.add.tileSprite(tileX, tileY, tileWidth, tileHeight, "battle_stats", spriteColor);
statSprite.setPipeline(this.scene.fieldSpritePipeline); statSprite.setPipeline(this.scene.fieldSpritePipeline);
statSprite.setAlpha(0); statSprite.setAlpha(0);
statSprite.setScale(6); statSprite.setScale(6);
statSprite.setOrigin(0.5, 1); statSprite.setOrigin(0.5, 1);
this.scene.playSound(`stat_${levels.value >= 1 ? "up" : "down"}`); this.scene.playSound(`stat_${stages.value >= 1 ? "up" : "down"}`);
statSprite.setMask(new Phaser.Display.Masks.BitmapMask(this.scene, pokemonMaskSprite ?? undefined)); statSprite.setMask(new Phaser.Display.Masks.BitmapMask(this.scene, pokemonMaskSprite ?? undefined));
@ -3590,7 +3580,7 @@ export class StatStageChangePhase extends PokemonPhase { // TODO: BattleStat
this.scene.tweens.add({ this.scene.tweens.add({
targets: statSprite, targets: statSprite,
duration: 1500, duration: 1500,
y: `${levels.value >= 1 ? "-" : "+"}=${160 * 6}` y: `${stages.value >= 1 ? "-" : "+"}=${160 * 6}`
}); });
this.scene.time.delayedCall(1750, () => { this.scene.time.delayedCall(1750, () => {
@ -3602,34 +3592,24 @@ export class StatStageChangePhase extends PokemonPhase { // TODO: BattleStat
} }
} }
getRandomStat(): BattleStat { aggregateStatStageChanges(): void {
const allStats = Utils.getEnumValues(BattleStat); const accEva: BattleStat[] = [ Stat.ACC, Stat.EVA ];
return this.getPokemon() ? allStats[this.getPokemon()!.randSeedInt(BattleStat.SPD + 1)] : BattleStat.ATK; // TODO: return default ATK on random? idk... const isAccEva = accEva.some(s => this.stats.includes(s));
} let existingPhase: StatStageChangePhase;
aggregateStatChanges(random: boolean = false): void {
const isAccEva = [BattleStat.ACC, BattleStat.EVA].some(s => this.stats.includes(s));
let existingPhase: StatChangePhase;
if (this.stats.length === 1) { if (this.stats.length === 1) {
while ((existingPhase = (this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex && p.stats.length === 1 while ((existingPhase = (this.scene.findPhase(p => p instanceof StatStageChangePhase && p.battlerIndex === this.battlerIndex && p.stats.length === 1
&& (p.stats[0] === this.stats[0] || (random && p.stats[0] === BattleStat.RAND)) && (p.stats[0] === this.stats[0])
&& p.selfTarget === this.selfTarget && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatChangePhase))) { && p.selfTarget === this.selfTarget && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatStageChangePhase))) {
if (existingPhase.stats[0] === BattleStat.RAND) { this.stages += existingPhase.stages;
existingPhase.stats[0] = this.getRandomStat();
if (existingPhase.stats[0] !== this.stats[0]) {
continue;
}
}
this.levels += existingPhase.levels;
if (!this.scene.tryRemovePhase(p => p === existingPhase)) { if (!this.scene.tryRemovePhase(p => p === existingPhase)) {
break; break;
} }
} }
} }
while ((existingPhase = (this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex && p.selfTarget === this.selfTarget while ((existingPhase = (this.scene.findPhase(p => p instanceof StatStageChangePhase && p.battlerIndex === this.battlerIndex && p.selfTarget === this.selfTarget
&& ([BattleStat.ACC, BattleStat.EVA].some(s => p.stats.includes(s)) === isAccEva) && (accEva.some(s => p.stats.includes(s)) === isAccEva)
&& p.levels === this.levels && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatChangePhase))) { && p.stages === this.stages && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatStageChangePhase))) {
this.stats.push(...existingPhase.stats); this.stats.push(...existingPhase.stats);
if (!this.scene.tryRemovePhase(p => p === existingPhase)) { if (!this.scene.tryRemovePhase(p => p === existingPhase)) {
break; break;
@ -3637,37 +3617,37 @@ export class StatStageChangePhase extends PokemonPhase { // TODO: BattleStat
} }
} }
getStatChangeMessages(stats: BattleStat[], levels: integer, relLevels: integer[]): string[] { getStatStageChangeMessages(stats: BattleStat[], stages: integer, relStages: integer[]): string[] {
const messages: string[] = []; const messages: string[] = [];
const relLevelStatIndexes = {}; const relStageStatIndexes = {};
for (let rl = 0; rl < relLevels.length; rl++) { for (let rl = 0; rl < relStages.length; rl++) {
const relLevel = relLevels[rl]; const relStage = relStages[rl];
if (!relLevelStatIndexes[relLevel]) { if (!relStageStatIndexes[relStage]) {
relLevelStatIndexes[relLevel] = []; relStageStatIndexes[relStage] = [];
} }
relLevelStatIndexes[relLevel].push(rl); relStageStatIndexes[relStage].push(rl);
} }
Object.keys(relLevelStatIndexes).forEach(rl => { Object.keys(relStageStatIndexes).forEach(rl => {
const relLevelStats = stats.filter((_, i) => relLevelStatIndexes[rl].includes(i)); const relStageStats = stats.filter((_, i) => relStageStatIndexes[rl].includes(i));
let statsFragment = ""; let statsFragment = "";
if (relLevelStats.length > 1) { if (relStageStats.length > 1) {
statsFragment = relLevelStats.length >= 5 statsFragment = relStageStats.length >= 5
? i18next.t("battle:stats") ? i18next.t("battle:stats")
: `${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]))}`; : `${relStageStats.slice(0, -1).map(s => i18next.t(getStatKey(s))).join(", ")}${relStageStats.length > 2 ? "," : ""} ${i18next.t("battle:statsAnd")} ${i18next.t(getStatKey(relStageStats[relStageStats.length - 1]))}`;
messages.push(i18next.t(getStatStageChangeDescriptionKey(Math.abs(parseInt(rl)), levels >= 1), { messages.push(i18next.t(getStatStageChangeDescriptionKey(Math.abs(parseInt(rl)), stages >= 1), {
pokemonNameWithAffix: getPokemonNameWithAffix(this.getPokemon()), pokemonNameWithAffix: getPokemonNameWithAffix(this.getPokemon()),
stats: statsFragment, stats: statsFragment,
count: relLevelStats.length count: relStageStats.length
})); }));
} else { } else {
statsFragment = i18next.t(getStatKey(relLevelStats[0])); statsFragment = i18next.t(getStatKey(relStageStats[0]));
messages.push(i18next.t(getStatStageChangeDescriptionKey(Math.abs(parseInt(rl)), levels >= 1), { messages.push(i18next.t(getStatStageChangeDescriptionKey(Math.abs(parseInt(rl)), stages >= 1), {
pokemonNameWithAffix: getPokemonNameWithAffix(this.getPokemon()), pokemonNameWithAffix: getPokemonNameWithAffix(this.getPokemon()),
stats: statsFragment, stats: statsFragment,
count: relLevelStats.length count: relStageStats.length
})); }));
} }
}); });

View File

@ -81,7 +81,7 @@ describe("Abilities - Intimidate", () => {
await game.phaseInterceptor.to(CommandPhase, false); await game.phaseInterceptor.to(CommandPhase, false);
const playerField = game.scene.getPlayerField()!; const playerField = game.scene.getPlayerField()!;
const enemyField = game.scene.getEnemyPokemon()!; const enemyField = game.scene.getEnemyField()!;
expect(enemyField[0].getStatStage(Stat.ATK)).toBe(-2); expect(enemyField[0].getStatStage(Stat.ATK)).toBe(-2);
expect(enemyField[1].getStatStage(Stat.ATK)).toBe(-2); expect(enemyField[1].getStatStage(Stat.ATK)).toBe(-2);

View File

@ -53,7 +53,8 @@ describe("Abilities - Moxie", () => {
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1);
}, 20000); }, 20000);
it("should raise ATK stat stage by 1 when defeating an ally Pokemon", async() => { // TODO: Activate this test when MOXIE is corrected to work faint and not battle victory
it.todo("should raise ATK stat stage by 1 when defeating an ally Pokemon", async() => {
game.override.battleType("double"); game.override.battleType("double");
const moveToUse = Moves.AERIAL_ACE; const moveToUse = Moves.AERIAL_ACE;
await game.startBattle([ await game.startBattle([

View File

@ -1,5 +1,5 @@
import { applyAbAttrs, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability"; import { applyAbAttrs, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability";
import { Stat } from "#app/data/pokemon-stat"; import { Stat } from "#enums/stat";
import { CommandPhase, MoveEffectPhase } from "#app/phases"; import { CommandPhase, MoveEffectPhase } from "#app/phases";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import { getMovePosition } from "#test/utils/gameManagerUtils"; import { getMovePosition } from "#test/utils/gameManagerUtils";

View File

@ -1,5 +1,5 @@
import { applyAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr, PostDefendTypeChangeAbAttr } from "#app/data/ability"; import { applyAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr, PostDefendTypeChangeAbAttr } from "#app/data/ability";
import { Stat } from "#app/data/pokemon-stat"; import { Stat } from "#enums/stat";
import { CommandPhase, MoveEffectPhase } from "#app/phases"; import { CommandPhase, MoveEffectPhase } from "#app/phases";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import { getMovePosition } from "#test/utils/gameManagerUtils"; import { getMovePosition } from "#test/utils/gameManagerUtils";

View File

@ -1,5 +1,5 @@
import { applyAbAttrs, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability"; import { applyAbAttrs, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability";
import { Stat } from "#app/data/pokemon-stat"; import { Stat } from "#enums/stat";
import { CommandPhase, MoveEffectPhase } from "#app/phases"; import { CommandPhase, MoveEffectPhase } from "#app/phases";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import { getMovePosition } from "#test/utils/gameManagerUtils"; import { getMovePosition } from "#test/utils/gameManagerUtils";

View File

@ -1,4 +1,4 @@
import { Stat } from "#app/data/pokemon-stat"; import { Stat } from "#enums/stat";
import { Status, StatusEffect } from "#app/data/status-effect.js"; import { Status, StatusEffect } from "#app/data/status-effect.js";
import { QuietFormChangePhase } from "#app/form-change-phase"; import { QuietFormChangePhase } from "#app/form-change-phase";
import { CommandPhase, DamagePhase, EnemyCommandPhase, MessagePhase, PostSummonPhase, SwitchPhase, SwitchSummonPhase, TurnEndPhase, TurnInitPhase, TurnStartPhase } from "#app/phases"; import { CommandPhase, DamagePhase, EnemyCommandPhase, MessagePhase, PostSummonPhase, SwitchPhase, SwitchSummonPhase, TurnEndPhase, TurnInitPhase, TurnStartPhase } from "#app/phases";

View File

@ -1,145 +0,0 @@
import { BattleStat, getStatKey } from "#enums/stat";
import { describe, expect, it } from "vitest";
import { arrayOfRange, mockI18next } from "./utils/testUtils";
const TEST_BATTLE_STAT = -99 as BattleStat;
const TEST_POKEMON = "Testmon";
const TEST_STAT = "Teststat";
describe("battle-stat", () => {
describe("getBattleStatName", () => {
it("should return the correct name for each BattleStat", () => {
mockI18next();
expect(getBattleStatName(BattleStat.ATK)).toBe("pokemonInfo:Stat.ATK");
expect(getBattleStatName(BattleStat.DEF)).toBe("pokemonInfo:Stat.DEF");
expect(getBattleStatName(BattleStat.SPATK)).toBe(
"pokemonInfo:Stat.SPATK"
);
expect(getBattleStatName(BattleStat.SPDEF)).toBe(
"pokemonInfo:Stat.SPDEF"
);
expect(getBattleStatName(BattleStat.SPD)).toBe("pokemonInfo:Stat.SPD");
expect(getBattleStatName(BattleStat.ACC)).toBe("pokemonInfo:Stat.ACC");
expect(getBattleStatName(BattleStat.EVA)).toBe("pokemonInfo:Stat.EVA");
});
it("should fall back to ??? for an unknown BattleStat", () => {
expect(getStatKey(TEST_BATTLE_STAT)).toBe("???");
});
});
describe("getBattleStatLevelChangeDescription", () => {
it("should return battle:statRose for +1", () => {
mockI18next();
const message = getBattleStatLevelChangeDescription(
TEST_POKEMON,
TEST_STAT,
1,
true
);
expect(message).toBe("battle:statRose");
});
it("should return battle:statSharplyRose for +2", () => {
mockI18next();
const message = getBattleStatLevelChangeDescription(
TEST_POKEMON,
TEST_STAT,
2,
true
);
expect(message).toBe("battle:statSharplyRose");
});
it("should return battle:statRoseDrastically for +3 to +6", () => {
mockI18next();
arrayOfRange(3, 6).forEach((n) => {
const message = getBattleStatLevelChangeDescription(
TEST_POKEMON,
TEST_STAT,
n,
true
);
expect(message).toBe("battle:statRoseDrastically");
});
});
it("should return battle:statWontGoAnyHigher for 7 or higher", () => {
mockI18next();
arrayOfRange(7, 10).forEach((n) => {
const message = getBattleStatLevelChangeDescription(
TEST_POKEMON,
TEST_STAT,
n,
true
);
expect(message).toBe("battle:statWontGoAnyHigher");
});
});
it("should return battle:statFell for -1", () => {
mockI18next();
const message = getBattleStatLevelChangeDescription(
TEST_POKEMON,
TEST_STAT,
1,
false
);
expect(message).toBe("battle:statFell");
});
it("should return battle:statHarshlyFell for -2", () => {
mockI18next();
const message = getBattleStatLevelChangeDescription(
TEST_POKEMON,
TEST_STAT,
2,
false
);
expect(message).toBe("battle:statHarshlyFell");
});
it("should return battle:statSeverelyFell for -3 to -6", () => {
mockI18next();
arrayOfRange(3, 6).forEach((n) => {
const message = getBattleStatLevelChangeDescription(
TEST_POKEMON,
TEST_STAT,
n,
false
);
expect(message).toBe("battle:statSeverelyFell");
});
});
it("should return battle:statWontGoAnyLower for -7 or lower", () => {
mockI18next();
arrayOfRange(7, 10).forEach((n) => {
const message = getBattleStatLevelChangeDescription(
TEST_POKEMON,
TEST_STAT,
n,
false
);
expect(message).toBe("battle:statWontGoAnyLower");
});
});
});
});

View File

@ -1,4 +1,4 @@
import { Stat } from "#app/data/pokemon-stat"; import { Stat } from "#enums/stat";
import { CommandPhase, EnemyCommandPhase, SelectTargetPhase, TurnStartPhase } from "#app/phases"; import { CommandPhase, EnemyCommandPhase, SelectTargetPhase, TurnStartPhase } from "#app/phases";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import { getMovePosition } from "#test/utils/gameManagerUtils"; import { getMovePosition } from "#test/utils/gameManagerUtils";

View File

@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest";
import Pokemon from "#app/field/pokemon.js"; import Pokemon from "#app/field/pokemon.js";
import BattleScene from "#app/battle-scene.js"; import BattleScene from "#app/battle-scene.js";
import { BattlerTag, BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags.js"; import { BattlerTag, BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags.js";
import { StatChangePhase } from "#app/phases.js"; import { StatStageChangePhase } from "#app/phases.js";
import { BattlerTagType } from "#app/enums/battler-tag-type.js"; import { BattlerTagType } from "#app/enums/battler-tag-type.js";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
@ -10,7 +10,7 @@ vi.mock("#app/battle-scene.js");
describe("BattlerTag - OctolockTag", () => { describe("BattlerTag - OctolockTag", () => {
describe("lapse behavior", () => { describe("lapse behavior", () => {
it("unshifts a StatChangePhase with expected stat stage changes", { timeout: 10000 }, async () => { it("unshifts a StatStageChangePhase with expected stat stage changes", { timeout: 10000 }, async () => {
const mockPokemon = { const mockPokemon = {
scene: new BattleScene(), scene: new BattleScene(),
getBattlerIndex: () => 0, getBattlerIndex: () => 0,
@ -19,9 +19,9 @@ describe("BattlerTag - OctolockTag", () => {
const subject = new OctolockTag(1); const subject = new OctolockTag(1);
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => { vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => {
expect(phase).toBeInstanceOf(StatChangePhase); expect(phase).toBeInstanceOf(StatStageChangePhase);
expect((phase as StatChangePhase)["stages"]).toEqual(-1); expect((phase as StatStageChangePhase)["stages"]).toEqual(-1);
expect((phase as StatChangePhase)["stats"]).toEqual([ Stat.DEF, Stat.SPDEF ]); expect((phase as StatStageChangePhase)["stats"]).toEqual([ Stat.DEF, Stat.SPDEF ]);
}); });
subject.lapse(mockPokemon, BattlerTagLapseType.TURN_END); subject.lapse(mockPokemon, BattlerTagLapseType.TURN_END);

View File

@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import Pokemon, { PokemonSummonData } from "#app/field/pokemon.js"; import Pokemon, { PokemonSummonData } from "#app/field/pokemon.js";
import BattleScene from "#app/battle-scene.js"; import BattleScene from "#app/battle-scene.js";
import { StockpilingTag } from "#app/data/battler-tags.js"; import { StockpilingTag } from "#app/data/battler-tags.js";
import { StatChangePhase } from "#app/phases.js"; import { StatStageChangePhase } from "#app/phases.js";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import * as messages from "#app/messages.js"; import * as messages from "#app/messages.js";
@ -12,7 +12,7 @@ beforeEach(() => {
describe("BattlerTag - StockpilingTag", () => { describe("BattlerTag - StockpilingTag", () => {
describe("onAdd", () => { describe("onAdd", () => {
it("unshifts a StatChangePhase with expected stat stage changes on add", { timeout: 10000 }, async () => { it("unshifts a StatStageChangePhase with expected stat stage changes on add", { timeout: 10000 }, async () => {
const mockPokemon = { const mockPokemon = {
scene: vi.mocked(new BattleScene()) as BattleScene, scene: vi.mocked(new BattleScene()) as BattleScene,
getBattlerIndex: () => 0, getBattlerIndex: () => 0,
@ -23,11 +23,11 @@ describe("BattlerTag - StockpilingTag", () => {
const subject = new StockpilingTag(1); const subject = new StockpilingTag(1);
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => { vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => {
expect(phase).toBeInstanceOf(StatChangePhase); expect(phase).toBeInstanceOf(StatStageChangePhase);
expect((phase as StatChangePhase)["stages"]).toEqual(1); expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ])); expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ]));
(phase as StatChangePhase)["onChange"]!(mockPokemon, [Stat.DEF, Stat.SPDEF], [1, 1]); (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [Stat.DEF, Stat.SPDEF], [1, 1]);
}); });
subject.onAdd(mockPokemon); subject.onAdd(mockPokemon);
@ -35,7 +35,7 @@ describe("BattlerTag - StockpilingTag", () => {
expect(mockPokemon.scene.unshiftPhase).toBeCalledTimes(1); expect(mockPokemon.scene.unshiftPhase).toBeCalledTimes(1);
}); });
it("unshifts a StatChangePhase with expected stat changes on add (one stat maxed)", { timeout: 10000 }, async () => { it("unshifts a StatStageChangePhase with expected stat changes on add (one stat maxed)", { timeout: 10000 }, async () => {
const mockPokemon = { const mockPokemon = {
scene: new BattleScene(), scene: new BattleScene(),
summonData: new PokemonSummonData(), summonData: new PokemonSummonData(),
@ -44,17 +44,17 @@ describe("BattlerTag - StockpilingTag", () => {
vi.spyOn(mockPokemon.scene, "queueMessage").mockImplementation(() => {}); vi.spyOn(mockPokemon.scene, "queueMessage").mockImplementation(() => {});
mockPokemon.setStatStage(Stat.DEF, 6); mockPokemon.summonData.statStages[Stat.DEF - 1] = 6;
mockPokemon.setStatStage(Stat.SPDEF, 5); mockPokemon.summonData.statStages[Stat.SPD - 1] = 5;
const subject = new StockpilingTag(1); const subject = new StockpilingTag(1);
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => { vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => {
expect(phase).toBeInstanceOf(StatChangePhase); expect(phase).toBeInstanceOf(StatStageChangePhase);
expect((phase as StatChangePhase)["stages"]).toEqual(1); expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([Stat.DEF, Stat.SPDEF])); expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([Stat.DEF, Stat.SPDEF]));
(phase as StatChangePhase)["onChange"]!(mockPokemon, [ Stat.DEF, Stat.SPDEF ], [1, 1]); (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [ Stat.DEF, Stat.SPDEF ], [1, 1]);
}); });
subject.onAdd(mockPokemon); subject.onAdd(mockPokemon);
@ -64,7 +64,7 @@ describe("BattlerTag - StockpilingTag", () => {
}); });
describe("onOverlap", () => { describe("onOverlap", () => {
it("unshifts a StatChangePhase with expected stat changes on overlap", { timeout: 10000 }, async () => { it("unshifts a StatStageChangePhase with expected stat changes on overlap", { timeout: 10000 }, async () => {
const mockPokemon = { const mockPokemon = {
scene: new BattleScene(), scene: new BattleScene(),
getBattlerIndex: () => 0, getBattlerIndex: () => 0,
@ -75,11 +75,11 @@ describe("BattlerTag - StockpilingTag", () => {
const subject = new StockpilingTag(1); const subject = new StockpilingTag(1);
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => { vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => {
expect(phase).toBeInstanceOf(StatChangePhase); expect(phase).toBeInstanceOf(StatStageChangePhase);
expect((phase as StatChangePhase)["stages"]).toEqual(1); expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ])); expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ]));
(phase as StatChangePhase)["onChange"]!(mockPokemon, [ Stat.DEF, Stat.SPDEF ], [1, 1]); (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [ Stat.DEF, Stat.SPDEF ], [1, 1]);
}); });
subject.onOverlap(mockPokemon); subject.onOverlap(mockPokemon);
@ -98,39 +98,39 @@ describe("BattlerTag - StockpilingTag", () => {
vi.spyOn(mockPokemon.scene, "queueMessage").mockImplementation(() => {}); vi.spyOn(mockPokemon.scene, "queueMessage").mockImplementation(() => {});
mockPokemon.setStatStage(Stat.DEF, 5); mockPokemon.summonData.statStages[Stat.DEF - 1] = 5;
mockPokemon.setStatStage(Stat.SPDEF, 4); mockPokemon.summonData.statStages[Stat.SPD - 1] = 4;
const subject = new StockpilingTag(1); const subject = new StockpilingTag(1);
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => { vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => {
expect(phase).toBeInstanceOf(StatChangePhase); expect(phase).toBeInstanceOf(StatStageChangePhase);
expect((phase as StatChangePhase)["stages"]).toEqual(1); expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ])); expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ]));
// def doesn't change // def doesn't change
(phase as StatChangePhase)["onChange"]!(mockPokemon, [ Stat.SPDEF ], [1]); (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [ Stat.SPDEF ], [1]);
}); });
subject.onAdd(mockPokemon); subject.onAdd(mockPokemon);
expect(subject.stockpiledCount).toBe(1); expect(subject.stockpiledCount).toBe(1);
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => { vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => {
expect(phase).toBeInstanceOf(StatChangePhase); expect(phase).toBeInstanceOf(StatStageChangePhase);
expect((phase as StatChangePhase)["stages"]).toEqual(1); expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ])); expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ]));
// def doesn't change // def doesn't change
(phase as StatChangePhase)["onChange"]!(mockPokemon, [ Stat.SPDEF ], [1]); (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [ Stat.SPDEF ], [1]);
}); });
subject.onOverlap(mockPokemon); subject.onOverlap(mockPokemon);
expect(subject.stockpiledCount).toBe(2); expect(subject.stockpiledCount).toBe(2);
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => { vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => {
expect(phase).toBeInstanceOf(StatChangePhase); expect(phase).toBeInstanceOf(StatStageChangePhase);
expect((phase as StatChangePhase)["stages"]).toEqual(1); expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ])); expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ]));
// neither stat changes, stack count should still increase // neither stat changes, stack count should still increase
}); });
@ -149,9 +149,9 @@ describe("BattlerTag - StockpilingTag", () => {
// removing tag should reverse stat changes // removing tag should reverse stat changes
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => { vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => {
expect(phase).toBeInstanceOf(StatChangePhase); expect(phase).toBeInstanceOf(StatStageChangePhase);
expect((phase as StatChangePhase)["stages"]).toEqual(-2); expect((phase as StatStageChangePhase)["stages"]).toEqual(-2);
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([Stat.SPDEF])); expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([Stat.SPDEF]));
}); });
subject.onRemove(mockPokemon); subject.onRemove(mockPokemon);

View File

@ -1,4 +1,4 @@
import { Stat } from "#app/data/pokemon-stat"; import { Stat } from "#enums/stat";
import { EvolutionStatBoosterModifier } from "#app/modifier/modifier"; import { EvolutionStatBoosterModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/modifier/modifier-type";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";

View File

@ -1,4 +1,4 @@
import { Stat } from "#app/data/pokemon-stat"; import { Stat } from "#enums/stat";
import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; import { SpeciesStatBoosterModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/modifier/modifier-type";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";

View File

@ -1,4 +1,4 @@
import { Stat } from "#app/data/pokemon-stat"; import { Stat } from "#enums/stat";
import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; import { SpeciesStatBoosterModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/modifier/modifier-type";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";

View File

@ -1,4 +1,4 @@
import { Stat } from "#app/data/pokemon-stat"; import { Stat } from "#enums/stat";
import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; import { SpeciesStatBoosterModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/modifier/modifier-type";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";

View File

@ -1,4 +1,4 @@
import { Stat } from "#app/data/pokemon-stat"; import { Stat } from "#enums/stat";
import { SpeciesStatBoosterModifier } from "#app/modifier/modifier"; import { SpeciesStatBoosterModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/modifier/modifier-type";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";

View File

@ -1,212 +0,0 @@
import { beforeAll, describe, expect, it } from "vitest";
import { getBattleStatName, getBattleStatLevelChangeDescription } from "#app/data/battle-stat.js";
import { BattleStat} from "#app/data/battle-stat.js";
import { pokemonInfo as enPokemonInfo } from "#app/locales/en/pokemon-info.js";
import { battle as enBattleStat } from "#app/locales/en/battle.js";
import { pokemonInfo as dePokemonInfo } from "#app/locales/de/pokemon-info.js";
import { battle as deBattleStat } from "#app/locales/de/battle.js";
import { pokemonInfo as esPokemonInfo } from "#app/locales/es/pokemon-info.js";
import { battle as esBattleStat } from "#app/locales/es/battle.js";
import { pokemonInfo as frPokemonInfo } from "#app/locales/fr/pokemon-info.js";
import { battle as frBattleStat } from "#app/locales/fr/battle.js";
import { pokemonInfo as itPokemonInfo } from "#app/locales/it/pokemon-info.js";
import { battle as itBattleStat } from "#app/locales/it/battle.js";
import { pokemonInfo as koPokemonInfo } from "#app/locales/ko/pokemon-info.js";
import { battle as koBattleStat } from "#app/locales/ko/battle.js";
import { pokemonInfo as ptBrPokemonInfo } from "#app/locales/pt_BR/pokemon-info.js";
import { battle as ptBrBattleStat } from "#app/locales/pt_BR/battle.js";
import { pokemonInfo as zhCnPokemonInfo } from "#app/locales/zh_CN/pokemon-info.js";
import { battle as zhCnBattleStat } from "#app/locales/zh_CN/battle.js";
import { pokemonInfo as zhTwPokemonInfo } from "#app/locales/zh_TW/pokemon-info.js";
import { battle as zhTwBattleStat } from "#app/locales/zh_TW/battle.js";
import i18next, { initI18n } from "#app/plugins/i18n";
import { KoreanPostpositionProcessor } from "i18next-korean-postposition-processor";
interface BattleStatTestUnit {
stat: BattleStat,
key: string
}
interface BattleStatLevelTestUnit {
levels: integer,
up: boolean,
key: string
changedStats: integer
}
function testBattleStatName(stat: BattleStat, expectMessage: string) {
const message = getBattleStatName(stat);
console.log(`message ${message}, expected ${expectMessage}`);
expect(message).toBe(expectMessage);
}
function testBattleStatLevelChangeDescription(levels: integer, up: boolean, expectMessage: string, changedStats: integer) {
const message = getBattleStatLevelChangeDescription("{{pokemonNameWithAffix}}", "{{stats}}", levels, up, changedStats);
console.log(`message ${message}, expected ${expectMessage}`);
expect(message).toBe(expectMessage);
}
describe("Test for BattleStat Localization", () => {
const battleStatUnits: BattleStatTestUnit[] = [];
const battleStatLevelUnits: BattleStatLevelTestUnit[] = [];
beforeAll(() => {
initI18n();
battleStatUnits.push({stat: BattleStat.ATK, key: "Stat.ATK"});
battleStatUnits.push({stat: BattleStat.DEF, key: "Stat.DEF"});
battleStatUnits.push({stat: BattleStat.SPATK, key: "Stat.SPATK"});
battleStatUnits.push({stat: BattleStat.SPDEF, key: "Stat.SPDEF"});
battleStatUnits.push({stat: BattleStat.SPD, key: "Stat.SPD"});
battleStatUnits.push({stat: BattleStat.ACC, key: "Stat.ACC"});
battleStatUnits.push({stat: BattleStat.EVA, key: "Stat.EVA"});
battleStatLevelUnits.push({levels: 1, up: true, key: "statRose_one", changedStats: 1});
battleStatLevelUnits.push({levels: 2, up: true, key: "statSharplyRose_one", changedStats: 1});
battleStatLevelUnits.push({levels: 3, up: true, key: "statRoseDrastically_one", changedStats: 1});
battleStatLevelUnits.push({levels: 4, up: true, key: "statRoseDrastically_one", changedStats: 1});
battleStatLevelUnits.push({levels: 5, up: true, key: "statRoseDrastically_one", changedStats: 1});
battleStatLevelUnits.push({levels: 6, up: true, key: "statRoseDrastically_one", changedStats: 1});
battleStatLevelUnits.push({levels: 7, up: true, key: "statWontGoAnyHigher_one", changedStats: 1});
battleStatLevelUnits.push({levels: 1, up: false, key: "statFell_one", changedStats: 1});
battleStatLevelUnits.push({levels: 2, up: false, key: "statHarshlyFell_one", changedStats: 1});
battleStatLevelUnits.push({levels: 3, up: false, key: "statSeverelyFell_one", changedStats: 1});
battleStatLevelUnits.push({levels: 4, up: false, key: "statSeverelyFell_one", changedStats: 1});
battleStatLevelUnits.push({levels: 5, up: false, key: "statSeverelyFell_one", changedStats: 1});
battleStatLevelUnits.push({levels: 6, up: false, key: "statSeverelyFell_one", changedStats: 1});
battleStatLevelUnits.push({levels: 7, up: false, key: "statWontGoAnyLower_one", changedStats: 1});
});
it("Test getBattleStatName() in English", async () => {
i18next.changeLanguage("en");
battleStatUnits.forEach(unit => {
testBattleStatName(unit.stat, enPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
});
});
it("Test getBattleStatLevelChangeDescription() in English", async () => {
i18next.changeLanguage("en");
battleStatLevelUnits.forEach(unit => {
testBattleStatLevelChangeDescription(unit.levels, unit.up, enBattleStat[unit.key], unit.changedStats);
});
});
it("Test getBattleStatName() in Español", async () => {
i18next.changeLanguage("es");
battleStatUnits.forEach(unit => {
testBattleStatName(unit.stat, esPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
});
});
it("Test getBattleStatLevelChangeDescription() in Español", async () => {
i18next.changeLanguage("es");
battleStatLevelUnits.forEach(unit => {
testBattleStatLevelChangeDescription(unit.levels, unit.up, esBattleStat[unit.key], unit.changedStats);
});
});
it("Test getBattleStatName() in Italiano", async () => {
i18next.changeLanguage("it");
battleStatUnits.forEach(unit => {
testBattleStatName(unit.stat, itPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
});
});
it("Test getBattleStatLevelChangeDescription() in Italiano", async () => {
i18next.changeLanguage("it");
battleStatLevelUnits.forEach(unit => {
testBattleStatLevelChangeDescription(unit.levels, unit.up, itBattleStat[unit.key], unit.changedStats);
});
});
it("Test getBattleStatName() in Français", async () => {
i18next.changeLanguage("fr");
battleStatUnits.forEach(unit => {
testBattleStatName(unit.stat, frPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
});
});
it("Test getBattleStatLevelChangeDescription() in Français", async () => {
i18next.changeLanguage("fr");
battleStatLevelUnits.forEach(unit => {
testBattleStatLevelChangeDescription(unit.levels, unit.up, frBattleStat[unit.key], unit.changedStats);
});
});
it("Test getBattleStatName() in Deutsch", async () => {
i18next.changeLanguage("de");
battleStatUnits.forEach(unit => {
testBattleStatName(unit.stat, dePokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
});
});
it("Test getBattleStatLevelChangeDescription() in Deutsch", async () => {
i18next.changeLanguage("de");
battleStatLevelUnits.forEach(unit => {
testBattleStatLevelChangeDescription(unit.levels, unit.up, deBattleStat[unit.key], unit.changedStats);
});
});
it("Test getBattleStatName() in Português (BR)", async () => {
i18next.changeLanguage("pt-BR");
battleStatUnits.forEach(unit => {
testBattleStatName(unit.stat, ptBrPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
});
});
it("Test getBattleStatLevelChangeDescription() in Português (BR)", async () => {
i18next.changeLanguage("pt-BR");
battleStatLevelUnits.forEach(unit => {
testBattleStatLevelChangeDescription(unit.levels, unit.up, ptBrBattleStat[unit.key], unit.changedStats);
});
});
it("Test getBattleStatName() in 简体中文", async () => {
i18next.changeLanguage("zh-CN");
battleStatUnits.forEach(unit => {
testBattleStatName(unit.stat, zhCnPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
});
});
it("Test getBattleStatLevelChangeDescription() in 简体中文", async () => {
i18next.changeLanguage("zh-CN");
battleStatLevelUnits.forEach(unit => {
// In i18next, the pluralization rules are language-specific, and Chinese only supports the _other suffix.
unit.key = unit.key.replace("one", "other");
testBattleStatLevelChangeDescription(unit.levels, unit.up, zhCnBattleStat[unit.key], unit.changedStats);
});
});
it("Test getBattleStatName() in 繁體中文", async () => {
i18next.changeLanguage("zh-TW");
battleStatUnits.forEach(unit => {
testBattleStatName(unit.stat, zhTwPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
});
});
it("Test getBattleStatLevelChangeDescription() in 繁體中文", async () => {
i18next.changeLanguage("zh-TW");
battleStatLevelUnits.forEach(unit => {
// In i18next, the pluralization rules are language-specific, and Chinese only supports the _other suffix.
unit.key = unit.key.replace("one", "other");
testBattleStatLevelChangeDescription(unit.levels, unit.up, zhTwBattleStat[unit.key], unit.changedStats);
});
});
it("Test getBattleStatName() in 한국어", async () => {
await i18next.changeLanguage("ko");
battleStatUnits.forEach(unit => {
testBattleStatName(unit.stat, koPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
});
});
it("Test getBattleStatLevelChangeDescription() in 한국어", async () => {
i18next.changeLanguage("ko", () => {
battleStatLevelUnits.forEach(unit => {
const processor = new KoreanPostpositionProcessor();
const message = processor.process(koBattleStat[unit.key]);
testBattleStatLevelChangeDescription(unit.levels, unit.up, message, unit.changedStats);
});
});
});
});

View File

@ -1,5 +1,5 @@
import { BattlerIndex } from "#app/battle.js"; import { BattlerIndex } from "#app/battle.js";
import { Stat } from "#app/data/pokemon-stat"; import { Stat } from "#enums/stat";
import { Abilities } from "#app/enums/abilities.js"; import { Abilities } from "#app/enums/abilities.js";
import { CommandPhase, SelectTargetPhase, TurnEndPhase } from "#app/phases"; import { CommandPhase, SelectTargetPhase, TurnEndPhase } from "#app/phases";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";

View File

@ -3,7 +3,7 @@ import Phaser from "phaser";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import { MoveEffectPhase, MovePhase, MoveEndPhase, DamagePhase } from "#app/phases"; import { MoveEffectPhase, MovePhase, MoveEndPhase, DamagePhase } from "#app/phases";
import { getMovePosition } from "#test/utils/gameManagerUtils"; import { getMovePosition } from "#test/utils/gameManagerUtils";
import { Stat } from "#app/data/pokemon-stat"; import { Stat } from "#enums/stat";
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { Species } from "#enums/species"; import { Species } from "#enums/species";

View File

@ -1,5 +1,5 @@
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { MoveEndPhase, StatChangePhase } from "#app/phases"; import { MoveEndPhase, StatStageChangePhase } from "#app/phases";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import { getMovePosition } from "#test/utils/gameManagerUtils"; import { getMovePosition } from "#test/utils/gameManagerUtils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
@ -60,7 +60,7 @@ describe("Moves - Make It Rain", () => {
game.doAttack(getMovePosition(game.scene, 0, Moves.MAKE_IT_RAIN)); game.doAttack(getMovePosition(game.scene, 0, Moves.MAKE_IT_RAIN));
await game.phaseInterceptor.to(StatChangePhase); await game.phaseInterceptor.to(StatStageChangePhase);
expect(enemyPokemon.isFainted()).toBe(true); expect(enemyPokemon.isFainted()).toBe(true);
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1);
@ -77,7 +77,7 @@ describe("Moves - Make It Rain", () => {
game.doAttack(getMovePosition(game.scene, 0, Moves.MAKE_IT_RAIN)); game.doAttack(getMovePosition(game.scene, 0, Moves.MAKE_IT_RAIN));
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
await game.phaseInterceptor.to(StatChangePhase); await game.phaseInterceptor.to(StatStageChangePhase);
enemyPokemon.forEach(p => expect(p.isFainted()).toBe(true)); enemyPokemon.forEach(p => expect(p.isFainted()).toBe(true));
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(-1);

View File

@ -1,5 +1,5 @@
import { BattlerIndex } from "#app/battle.js"; import { BattlerIndex } from "#app/battle.js";
import { Stat } from "#app/data/pokemon-stat"; import { Stat } from "#enums/stat";
import { CommandPhase, SelectTargetPhase, TurnEndPhase } from "#app/phases"; import { CommandPhase, SelectTargetPhase, TurnEndPhase } from "#app/phases";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import { getMovePosition } from "#test/utils/gameManagerUtils"; import { getMovePosition } from "#test/utils/gameManagerUtils";

View File

@ -1,4 +1,4 @@
import { Stat } from "#app/data/pokemon-stat"; import { Stat } from "#enums/stat";
import { CommandPhase, EnemyCommandPhase, TurnEndPhase } from "#app/phases"; import { CommandPhase, EnemyCommandPhase, TurnEndPhase } from "#app/phases";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import { getMovePosition } from "#test/utils/gameManagerUtils"; import { getMovePosition } from "#test/utils/gameManagerUtils";

View File

@ -1,5 +1,5 @@
import { ArenaTagSide } from "#app/data/arena-tag.js"; import { ArenaTagSide } from "#app/data/arena-tag.js";
import { Stat } from "#app/data/pokemon-stat.js"; import { Stat } from "#enums/stat";
import { ArenaTagType } from "#app/enums/arena-tag-type.js"; import { ArenaTagType } from "#app/enums/arena-tag-type.js";
import { TurnEndPhase } from "#app/phases"; import { TurnEndPhase } from "#app/phases";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";

View File

@ -22,7 +22,7 @@ import {
SelectTargetPhase, SelectTargetPhase,
ShinySparklePhase, ShinySparklePhase,
ShowAbilityPhase, ShowAbilityPhase,
StatChangePhase, StatStageChangePhase,
SummonPhase, SummonPhase,
SwitchPhase, SwitchPhase,
SwitchSummonPhase, SwitchSummonPhase,
@ -85,7 +85,7 @@ export default class PhaseInterceptor {
[NewBattlePhase, this.startPhase], [NewBattlePhase, this.startPhase],
[VictoryPhase, this.startPhase], [VictoryPhase, this.startPhase],
[MoveEndPhase, this.startPhase], [MoveEndPhase, this.startPhase],
[StatChangePhase, this.startPhase], [StatStageChangePhase, this.startPhase],
[ShinySparklePhase, this.startPhase], [ShinySparklePhase, this.startPhase],
[SelectTargetPhase, this.startPhase], [SelectTargetPhase, this.startPhase],
[UnavailablePhase, this.startPhase], [UnavailablePhase, this.startPhase],

View File

@ -69,8 +69,8 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
public flyoutMenu?: BattleFlyout; public flyoutMenu?: BattleFlyout;
private statOrder: Stat[]; private statOrder: Stat[];
private statOrderPlayer = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA, Stat.SPD ]; private readonly 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 ]; private readonly statOrderEnemy = [ Stat.HP, Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA, Stat.SPD ];
constructor(scene: Phaser.Scene, x: number, y: number, player: boolean) { constructor(scene: Phaser.Scene, x: number, y: number, player: boolean) {
super(scene, x, y); super(scene, x, y);
@ -650,12 +650,12 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
this.lastLevel = pokemon.level; this.lastLevel = pokemon.level;
} }
const battleStats = pokemon.getStatStages(); const stats = pokemon.getStatStages();
const battleStatsStr = battleStats.join(""); const statsStr = stats.join("");
if (this.lastStats !== battleStatsStr) { if (this.lastStats !== statsStr) {
this.updateStats(battleStats); this.updateStats(stats);
this.lastStats = battleStatsStr; this.lastStats = statsStr;
} }
this.shinyIcon.setVisible(pokemon.isShiny()); this.shinyIcon.setVisible(pokemon.isShiny());
@ -770,7 +770,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
updateStats(stats: integer[]): void { updateStats(stats: integer[]): void {
this.statOrder.map((s, i) => { this.statOrder.map((s, i) => {
if (s !== Stat.HP) { if (s !== Stat.HP) {
this.statNumbers[i].setFrame(stats[s].toString()); this.statNumbers[i].setFrame(stats[s - 1].toString());
} }
}); });
} }

View File

@ -2,6 +2,7 @@ import BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodete
import BattleScene from "../battle-scene"; import BattleScene from "../battle-scene";
import { TextStyle, addBBCodeTextObject, addTextObject, getTextColor } from "./text"; import { TextStyle, addBBCodeTextObject, addTextObject, getTextColor } from "./text";
import { getStatKey, PERMANENT_STATS } from "#app/enums/stat.js"; import { getStatKey, PERMANENT_STATS } from "#app/enums/stat.js";
import i18next from "i18next";
const ivChartSize = 24; 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]]; const ivChartStatCoordMultipliers = [[0, -1], [0.825, -0.5], [0.825, 0.5], [-0.825, -0.5], [-0.825, 0.5], [0, 1]];
@ -54,7 +55,7 @@ export class StatsContainer extends Phaser.GameObjects.Container {
this.ivStatValueTexts = []; this.ivStatValueTexts = [];
for (const s of PERMANENT_STATS) { 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); const statLabel = addTextObject(this.scene, ivChartBg.x + (ivChartSize) * ivChartStatCoordMultipliers[s][0] * 1.325, ivChartBg.y + (ivChartSize) * ivChartStatCoordMultipliers[s][1] * 1.325 - 4 + ivLabelOffset[s], i18next.t(getStatKey(s)), TextStyle.TOOLTIP_CONTENT);
statLabel.setOrigin(0.5); statLabel.setOrigin(0.5);
this.ivStatValueTexts[s] = addBBCodeTextObject(this.scene, statLabel.x, statLabel.y + 8, "0", TextStyle.TOOLTIP_CONTENT); this.ivStatValueTexts[s] = addBBCodeTextObject(this.scene, statLabel.x, statLabel.y + 8, "0", TextStyle.TOOLTIP_CONTENT);