[Refactor/Bug/Move] Overhaul Stats and Battle Items, Implement Several Stat Moves (#2699)

* Create Getters, Setters, and Types

* Work on `pokemon.ts`

* Adjust Types, Refactor `White Herb` Modifier

* Migrate `TempBattleStat` Usage

* Refactor `PokemonBaseStatModifier` Slightly

* Remove `BattleStat`, Use "Stat Stages" & New Names

* Address Phase `integers`

* Finalize `BattleStat` Removal

* Address Minor Manual NITs

* Apply Own Review Suggestions

* Fix Syntax Error

* Add Docs

* Overhaul X Items

* Implement Guard and Power Split with Unit Tests

* Add Several Unit Tests and Fixes

* Implement Speed Swap with Unit Tests

* Fix Keys in Summary Menu

* Fix Starf Berry Raising EVA and ACC

* Fix Contrary & Simple, Verify with Unit Tests

* Implement Power & Guard Swap with Unit Tests

* Add Move Effect Message to Speed Swap

* Add Move Effect Message to Power & Guard Split

* Add Localization Entries

* Adjust Last X Item Unit Test

* Overhaul X Items Unit Tests

* Finish Missing Docs

* Revamp Crit-Based Unit Tests & Dire Hit

* Address Initial NITs

* Apply NIT Batch

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Fix Moody Test

* Address Multiple Messages for `ProtectStatAbAttr`

* Change `ignoreOverride` to `bypassSummonData`

* Adjust Italian Localization

Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com>

* Fix Moody

---------

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com>
This commit is contained in:
Amani H. 2024-09-02 22:12:34 -04:00 committed by GitHub
parent 947400f2b9
commit 89e80f3deb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
150 changed files with 3868 additions and 3225 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.

File diff suppressed because it is too large Load Diff

View File

@ -7,17 +7,17 @@ import Pokemon, { HitResult, PokemonMove } from "../field/pokemon";
import { StatusEffect } from "./status-effect"; import { StatusEffect } from "./status-effect";
import { BattlerIndex } from "../battle"; import { BattlerIndex } from "../battle";
import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability"; import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability";
import { BattleStat } from "./battle-stat"; import { Stat } from "#enums/stat";
import { CommonAnim, CommonBattleAnim } from "./battle-anims"; import { CommonAnim, CommonBattleAnim } from "./battle-anims";
import i18next from "i18next"; import i18next from "i18next";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { MoveEffectPhase } from "#app/phases/move-effect-phase.js"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase.js"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { StatChangePhase } from "#app/phases/stat-change-phase.js"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
export enum ArenaTagSide { export enum ArenaTagSide {
BOTH, BOTH,
@ -786,8 +786,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, [BattleStat.SPD], statLevels.value)); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value));
} }
} }
@ -875,7 +875,7 @@ class TailwindTag extends ArenaTag {
// Raise attack by one stage if party member has WIND_RIDER ability // 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, [BattleStat.ATK], 1, true)); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.ATK ], 1, true));
} }
} }
} }

View File

@ -1,71 +0,0 @@
import i18next, { ParseKeys } from "i18next";
export enum BattleStat {
ATK,
DEF,
SPATK,
SPDEF,
SPD,
ACC,
EVA,
RAND,
HP
}
export function getBattleStatName(stat: BattleStat) {
switch (stat) {
case BattleStat.ATK:
return i18next.t("pokemonInfo:Stat.ATK");
case BattleStat.DEF:
return i18next.t("pokemonInfo:Stat.DEF");
case BattleStat.SPATK:
return i18next.t("pokemonInfo:Stat.SPATK");
case BattleStat.SPDEF:
return i18next.t("pokemonInfo:Stat.SPDEF");
case BattleStat.SPD:
return i18next.t("pokemonInfo:Stat.SPD");
case BattleStat.ACC:
return i18next.t("pokemonInfo:Stat.ACC");
case BattleStat.EVA:
return i18next.t("pokemonInfo:Stat.EVA");
case BattleStat.HP:
return i18next.t("pokemonInfo:Stat.HPStat");
default:
return "???";
}
}
export function getBattleStatLevelChangeDescription(pokemonNameWithAffix: string, stats: string, levels: integer, up: boolean, count: number = 1) {
const stringKey = (() => {
if (up) {
switch (levels) {
case 1:
return "battle:statRose";
case 2:
return "battle:statSharplyRose";
case 3:
case 4:
case 5:
case 6:
return "battle:statRoseDrastically";
default:
return "battle:statWontGoAnyHigher";
}
} else {
switch (levels) {
case 1:
return "battle:statFell";
case 2:
return "battle:statHarshlyFell";
case 3:
case 4:
case 5:
case 6:
return "battle:statSeverelyFell";
default:
return "battle:statWontGoAnyLower";
}
}
})();
return i18next.t(stringKey as ParseKeys, { pokemonNameWithAffix, stats, count });
}

View File

@ -1,7 +1,6 @@
import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "./battle-anims"; import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "./battle-anims";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { MoveResult, HitResult } from "../field/pokemon"; import Pokemon, { MoveResult, HitResult } from "../field/pokemon";
import { Stat, getStatName } from "./pokemon-stat";
import { StatusEffect } from "./status-effect"; import { StatusEffect } from "./status-effect";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { ChargeAttr, MoveFlags, allMoves } from "./move"; import { ChargeAttr, MoveFlags, allMoves } from "./move";
@ -9,20 +8,20 @@ import { Type } from "./type";
import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs } from "./ability"; import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs } from "./ability";
import { TerrainType } from "./terrain"; import { TerrainType } from "./terrain";
import { WeatherType } from "./weather"; import { WeatherType } from "./weather";
import { BattleStat } from "./battle-stat";
import { allAbilities } from "./ability"; import { allAbilities } from "./ability";
import { SpeciesFormChangeManualTrigger } from "./pokemon-forms"; import { SpeciesFormChangeManualTrigger } from "./pokemon-forms";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import i18next from "#app/plugins/i18n.js"; import i18next from "#app/plugins/i18n";
import { CommonAnimPhase } from "#app/phases/common-anim-phase.js"; import { Stat, type BattleStat, type EffectiveStat, EFFECTIVE_STATS, getStatKey } from "#app/enums/stat";
import { MoveEffectPhase } from "#app/phases/move-effect-phase.js"; import { CommonAnimPhase } from "#app/phases/common-anim-phase";
import { MovePhase } from "#app/phases/move-phase.js"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js"; import { MovePhase } from "#app/phases/move-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase.js"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { StatChangePhase, StatChangeCallback } from "#app/phases/stat-change-phase.js"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { StatStageChangePhase, StatStageChangeCallback } from "#app/phases/stat-stage-change-phase";
export enum BattlerTagLapseType { export enum BattlerTagLapseType {
FAINT, FAINT,
@ -362,8 +361,8 @@ export class ConfusedTag extends BattlerTag {
// 1/3 chance of hitting self with a 40 base power move // 1/3 chance of hitting self with a 40 base power move
if (pokemon.randSeedInt(3) === 0) { if (pokemon.randSeedInt(3) === 0) {
const atk = pokemon.getBattleStat(Stat.ATK); const atk = pokemon.getEffectiveStat(Stat.ATK);
const def = pokemon.getBattleStat(Stat.DEF); const def = pokemon.getEffectiveStat(Stat.DEF);
const damage = Utils.toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedInt(15, 85) / 100)); const damage = Utils.toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedInt(15, 85) / 100));
pokemon.scene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself")); pokemon.scene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
pokemon.damageAndUpdate(damage); pokemon.damageAndUpdate(damage);
@ -767,7 +766,7 @@ export class OctolockTag extends TrappedTag {
const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType); const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);
if (shouldLapse) { if (shouldLapse) {
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [BattleStat.DEF, BattleStat.SPDEF], -1)); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.DEF, Stat.SPDEF ], -1));
return true; return true;
} }
@ -1093,7 +1092,7 @@ export class ContactDamageProtectedTag extends ProtectedTag {
} }
} }
export class ContactStatChangeProtectedTag extends ProtectedTag { export class ContactStatStageChangeProtectedTag extends ProtectedTag {
private stat: BattleStat; private stat: BattleStat;
private levels: number; private levels: number;
@ -1110,7 +1109,7 @@ export class ContactStatChangeProtectedTag extends ProtectedTag {
*/ */
loadTag(source: BattlerTag | any): void { loadTag(source: BattlerTag | any): void {
super.loadTag(source); super.loadTag(source);
this.stat = source.stat as BattleStat; this.stat = source.stat;
this.levels = source.levels; this.levels = source.levels;
} }
@ -1121,7 +1120,7 @@ export class ContactStatChangeProtectedTag extends ProtectedTag {
const effectPhase = pokemon.scene.getCurrentPhase(); const effectPhase = pokemon.scene.getCurrentPhase();
if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) { if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) {
const attacker = effectPhase.getPokemon(); const attacker = effectPhase.getPokemon();
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, attacker.getBattlerIndex(), true, [ this.stat ], this.levels)); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, attacker.getBattlerIndex(), true, [ this.stat ], this.levels));
} }
} }
@ -1348,11 +1347,10 @@ export class HighestStatBoostTag extends AbilityBattlerTag {
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
super.onAdd(pokemon); super.onAdd(pokemon);
const stats = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ]; let highestStat: EffectiveStat;
let highestStat: Stat; EFFECTIVE_STATS.map(s => pokemon.getEffectiveStat(s)).reduce((highestValue: number, value: number, i: number) => {
stats.map(s => pokemon.getBattleStat(s)).reduce((highestValue: number, value: number, i: number) => {
if (value > highestValue) { if (value > highestValue) {
highestStat = stats[i]; highestStat = EFFECTIVE_STATS[i];
return value; return value;
} }
return highestValue; return highestValue;
@ -1370,7 +1368,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag {
break; break;
} }
pokemon.scene.queueMessage(i18next.t("battlerTags:highestStatBoostOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), statName: getStatName(highestStat) }), null, false, null, true); pokemon.scene.queueMessage(i18next.t("battlerTags:highestStatBoostOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), statName: i18next.t(getStatKey(highestStat)) }), null, false, null, true);
} }
onRemove(pokemon: Pokemon): void { onRemove(pokemon: Pokemon): void {
@ -1714,25 +1712,25 @@ export class IceFaceBlockDamageTag extends FormBlockDamageTag {
*/ */
export class StockpilingTag extends BattlerTag { export class StockpilingTag extends BattlerTag {
public stockpiledCount: number = 0; public stockpiledCount: number = 0;
public statChangeCounts: { [BattleStat.DEF]: number; [BattleStat.SPDEF]: number } = { public statChangeCounts: { [Stat.DEF]: number; [Stat.SPDEF]: number } = {
[BattleStat.DEF]: 0, [Stat.DEF]: 0,
[BattleStat.SPDEF]: 0 [Stat.SPDEF]: 0
}; };
constructor(sourceMove: Moves = Moves.NONE) { constructor(sourceMove: Moves = Moves.NONE) {
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(BattleStat.DEF)] ?? 0; const defChange = statChanges[statsChanged.indexOf(Stat.DEF)] ?? 0;
const spDefChange = statChanges[statsChanged.indexOf(BattleStat.SPDEF)] ?? 0; const spDefChange = statChanges[statsChanged.indexOf(Stat.SPDEF)] ?? 0;
if (defChange) { if (defChange) {
this.statChangeCounts[BattleStat.DEF]++; this.statChangeCounts[Stat.DEF]++;
} }
if (spDefChange) { if (spDefChange) {
this.statChangeCounts[BattleStat.SPDEF]++; this.statChangeCounts[Stat.SPDEF]++;
} }
}; };
@ -1740,8 +1738,8 @@ export class StockpilingTag extends BattlerTag {
super.loadTag(source); super.loadTag(source);
this.stockpiledCount = source.stockpiledCount || 0; this.stockpiledCount = source.stockpiledCount || 0;
this.statChangeCounts = { this.statChangeCounts = {
[ BattleStat.DEF ]: source.statChangeCounts?.[ BattleStat.DEF ] ?? 0, [ Stat.DEF ]: source.statChangeCounts?.[ Stat.DEF ] ?? 0,
[ BattleStat.SPDEF ]: source.statChangeCounts?.[ BattleStat.SPDEF ] ?? 0, [ Stat.SPDEF ]: source.statChangeCounts?.[ Stat.SPDEF ] ?? 0,
}; };
} }
@ -1761,9 +1759,9 @@ export class StockpilingTag extends BattlerTag {
})); }));
// Attempt to increase DEF and SPDEF by one stage, keeping track of successful changes. // 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, pokemon.scene, pokemon.getBattlerIndex(), true,
[BattleStat.SPDEF, BattleStat.DEF], 1, true, false, true, this.onStatsChanged [Stat.SPDEF, Stat.DEF], 1, true, false, true, this.onStatStagesChanged
)); ));
} }
} }
@ -1777,15 +1775,15 @@ export class StockpilingTag extends BattlerTag {
* one stage for each stack which had successfully changed that particular stat during onAdd. * one stage for each stack which had successfully changed that particular stat during onAdd.
*/ */
onRemove(pokemon: Pokemon): void { onRemove(pokemon: Pokemon): void {
const defChange = this.statChangeCounts[BattleStat.DEF]; const defChange = this.statChangeCounts[Stat.DEF];
const spDefChange = this.statChangeCounts[BattleStat.SPDEF]; const spDefChange = this.statChangeCounts[Stat.SPDEF];
if (defChange) { 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) { if (spDefChange) {
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.SPDEF], -spDefChange, true, false, true)); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPDEF ], -spDefChange, true, false, true));
} }
} }
} }
@ -1927,11 +1925,11 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
case BattlerTagType.SPIKY_SHIELD: case BattlerTagType.SPIKY_SHIELD:
return new ContactDamageProtectedTag(sourceMove, 8); return new ContactDamageProtectedTag(sourceMove, 8);
case BattlerTagType.KINGS_SHIELD: case BattlerTagType.KINGS_SHIELD:
return new ContactStatChangeProtectedTag(sourceMove, tagType, BattleStat.ATK, -1); return new ContactStatStageChangeProtectedTag(sourceMove, tagType, Stat.ATK, -1);
case BattlerTagType.OBSTRUCT: case BattlerTagType.OBSTRUCT:
return new ContactStatChangeProtectedTag(sourceMove, tagType, BattleStat.DEF, -2); return new ContactStatStageChangeProtectedTag(sourceMove, tagType, Stat.DEF, -2);
case BattlerTagType.SILK_TRAP: case BattlerTagType.SILK_TRAP:
return new ContactStatChangeProtectedTag(sourceMove, tagType, BattleStat.SPD, -1); return new ContactStatStageChangeProtectedTag(sourceMove, tagType, Stat.SPD, -1);
case BattlerTagType.BANEFUL_BUNKER: case BattlerTagType.BANEFUL_BUNKER:
return new ContactPoisonProtectedTag(sourceMove); return new ContactPoisonProtectedTag(sourceMove);
case BattlerTagType.BURNING_BULWARK: case BattlerTagType.BURNING_BULWARK:

View File

@ -1,14 +1,14 @@
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { HitResult } from "../field/pokemon"; import Pokemon, { HitResult } from "../field/pokemon";
import { BattleStat } from "./battle-stat";
import { getStatusEffectHealText } from "./status-effect"; import { getStatusEffectHealText } from "./status-effect";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { DoubleBerryEffectAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs } from "./ability"; import { DoubleBerryEffectAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs } from "./ability";
import i18next from "i18next"; import i18next from "i18next";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js"; import { Stat, type BattleStat } from "#app/enums/stat";
import { StatChangePhase } from "#app/phases/stat-change-phase.js"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
export function getBerryName(berryType: BerryType): string { export function getBerryName(berryType: BerryType): string {
return i18next.t(`berry:${BerryType[berryType]}.name`); return i18next.t(`berry:${BerryType[berryType]}.name`);
@ -35,9 +35,10 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate {
case BerryType.SALAC: case BerryType.SALAC:
return (pokemon: Pokemon) => { return (pokemon: Pokemon) => {
const threshold = new Utils.NumberHolder(0.25); const threshold = new Utils.NumberHolder(0.25);
const battleStat = (berryType - BerryType.LIECHI) as BattleStat; // Offset BerryType such that LIECHI -> Stat.ATK = 1, GANLON -> Stat.DEF = 2, so on and so forth
const stat: BattleStat = berryType - BerryType.ENIGMA;
applyAbAttrs(ReduceBerryUseThresholdAbAttr, pokemon, null, false, threshold); applyAbAttrs(ReduceBerryUseThresholdAbAttr, pokemon, null, false, threshold);
return pokemon.getHpRatio() < threshold.value && pokemon.summonData.battleStats[battleStat] < 6; return pokemon.getHpRatio() < threshold.value && pokemon.getStatStage(stat) < 6;
}; };
case BerryType.LANSAT: case BerryType.LANSAT:
return (pokemon: Pokemon) => { return (pokemon: Pokemon) => {
@ -95,10 +96,11 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
if (pokemon.battleData) { if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType); pokemon.battleData.berriesEaten.push(berryType);
} }
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 statLevels = new Utils.NumberHolder(1); const stat: BattleStat = berryType - BerryType.ENIGMA;
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statLevels); const statStages = new Utils.NumberHolder(1);
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ battleStat ], statLevels.value)); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statStages);
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) => {
@ -112,9 +114,10 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
if (pokemon.battleData) { if (pokemon.battleData) {
pokemon.battleData.berriesEaten.push(berryType); pokemon.battleData.berriesEaten.push(berryType);
} }
const statLevels = new Utils.NumberHolder(2); const randStat = Utils.randSeedInt(Stat.SPD, Stat.ATK);
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statLevels); const stages = new Utils.NumberHolder(2);
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ BattleStat.RAND ], statLevels.value)); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, stages);
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) => {

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
import { Stat, getStatName } from "./pokemon-stat";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { TextStyle, getBBCodeFrag } from "../ui/text"; import { TextStyle, getBBCodeFrag } from "../ui/text";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { UiTheme } from "#enums/ui-theme"; import { UiTheme } from "#enums/ui-theme";
import i18next from "i18next"; import i18next from "i18next";
import { Stat, EFFECTIVE_STATS, getShortenedStatKey } from "#app/enums/stat";
export { Nature }; export { Nature };
@ -14,10 +14,9 @@ export function getNatureName(nature: Nature, includeStatEffects: boolean = fals
ret = i18next.t("nature:" + ret as any); ret = i18next.t("nature:" + ret as any);
} }
if (includeStatEffects) { if (includeStatEffects) {
const stats = Utils.getEnumValues(Stat).slice(1);
let increasedStat: Stat | null = null; let increasedStat: Stat | null = null;
let decreasedStat: Stat | null = null; let decreasedStat: Stat | null = null;
for (const stat of stats) { for (const stat of EFFECTIVE_STATS) {
const multiplier = getNatureStatMultiplier(nature, stat); const multiplier = getNatureStatMultiplier(nature, stat);
if (multiplier > 1) { if (multiplier > 1) {
increasedStat = stat; increasedStat = stat;
@ -28,7 +27,7 @@ export function getNatureName(nature: Nature, includeStatEffects: boolean = fals
const textStyle = forStarterSelect ? TextStyle.SUMMARY_ALT : TextStyle.WINDOW; const textStyle = forStarterSelect ? TextStyle.SUMMARY_ALT : TextStyle.WINDOW;
const getTextFrag = !ignoreBBCode ? (text: string, style: TextStyle) => getBBCodeFrag(text, style, uiTheme) : (text: string, style: TextStyle) => text; const getTextFrag = !ignoreBBCode ? (text: string, style: TextStyle) => getBBCodeFrag(text, style, uiTheme) : (text: string, style: TextStyle) => text;
if (increasedStat && decreasedStat) { if (increasedStat && decreasedStat) {
ret = `${getTextFrag(`${ret}${!forStarterSelect ? "\n" : " "}(`, textStyle)}${getTextFrag(`+${getStatName(increasedStat, true)}`, TextStyle.SUMMARY_PINK)}${getTextFrag("/", textStyle)}${getTextFrag(`-${getStatName(decreasedStat, true)}`, TextStyle.SUMMARY_BLUE)}${getTextFrag(")", textStyle)}`; ret = `${getTextFrag(`${ret}${!forStarterSelect ? "\n" : " "}(`, textStyle)}${getTextFrag(`+${i18next.t(getShortenedStatKey(increasedStat))}`, TextStyle.SUMMARY_PINK)}${getTextFrag("/", textStyle)}${getTextFrag(`-${i18next.t(getShortenedStatKey(decreasedStat))}`, TextStyle.SUMMARY_BLUE)}${getTextFrag(")", textStyle)}`;
} else { } else {
ret = getTextFrag(`${ret}${!forStarterSelect ? "\n" : " "}(-)`, textStyle); ret = getTextFrag(`${ret}${!forStarterSelect ? "\n" : " "}(-)`, textStyle);
} }

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

@ -14,7 +14,7 @@ import { GrowthRate } from "./exp";
import { EvolutionLevel, SpeciesWildEvolutionDelay, pokemonEvolutions, pokemonPrevolutions } from "./pokemon-evolutions"; import { EvolutionLevel, SpeciesWildEvolutionDelay, pokemonEvolutions, pokemonPrevolutions } from "./pokemon-evolutions";
import { Type } from "./type"; import { Type } from "./type";
import { LevelMoves, pokemonFormLevelMoves, pokemonFormLevelMoves as pokemonSpeciesFormLevelMoves, pokemonSpeciesLevelMoves } from "./pokemon-level-moves"; import { LevelMoves, pokemonFormLevelMoves, pokemonFormLevelMoves as pokemonSpeciesFormLevelMoves, pokemonSpeciesLevelMoves } from "./pokemon-level-moves";
import { Stat } from "./pokemon-stat"; import { Stat } from "#enums/stat";
import { Variant, VariantSet, variantColorCache, variantData } from "./variant"; import { Variant, VariantSet, variantColorCache, variantData } from "./variant";
export enum Region { export enum Region {

View File

@ -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;
}

View File

@ -1,38 +0,0 @@
import { BattleStat, getBattleStatName } from "./battle-stat";
import i18next from "i18next";
export enum TempBattleStat {
ATK,
DEF,
SPATK,
SPDEF,
SPD,
ACC,
CRIT
}
export function getTempBattleStatName(tempBattleStat: TempBattleStat) {
if (tempBattleStat === TempBattleStat.CRIT) {
return i18next.t("modifierType:TempBattleStatBoosterStatName.CRIT");
}
return getBattleStatName(tempBattleStat as integer as BattleStat);
}
export function getTempBattleStatBoosterItemName(tempBattleStat: TempBattleStat) {
switch (tempBattleStat) {
case TempBattleStat.ATK:
return "X Attack";
case TempBattleStat.DEF:
return "X Defense";
case TempBattleStat.SPATK:
return "X Sp. Atk";
case TempBattleStat.SPDEF:
return "X Sp. Def";
case TempBattleStat.SPD:
return "X Speed";
case TempBattleStat.ACC:
return "X Accuracy";
case TempBattleStat.CRIT:
return "Dire Hit";
}
}

View File

@ -1,8 +1,75 @@
/** Enum that comprises all possible stat-related attributes, in-battle and permanent, of a Pokemon. */
export enum Stat { export enum Stat {
/** Hit Points */
HP = 0, HP = 0,
/** Attack */
ATK, ATK,
/** Defense */
DEF, DEF,
/** Special Attack */
SPATK, SPATK,
/** Special Defense */
SPDEF, SPDEF,
/** Speed */
SPD, SPD,
/** Accuracy */
ACC,
/** Evasiveness */
EVA
}
/** A constant array comprised of the {@linkcode Stat} values that make up {@linkcode PermanentStat}. */
export const PERMANENT_STATS = [ Stat.HP, Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ] as const;
/** Type used to describe the core, permanent stats of a Pokemon. */
export type PermanentStat = typeof PERMANENT_STATS[number];
/** A constant array comprised of the {@linkcode Stat} values that make up {@linkcode EFfectiveStat}. */
export const EFFECTIVE_STATS = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ] as const;
/** Type used to describe the intersection of core stats and stats that have stages in battle. */
export type EffectiveStat = typeof EFFECTIVE_STATS[number];
/** A constant array comprised of {@linkcode Stat} the values that make up {@linkcode BattleStat}. */
export const BATTLE_STATS = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD, Stat.ACC, Stat.EVA ] as const;
/** Type used to describe the stats that have stages which can be incremented and decremented in battle. */
export type BattleStat = typeof BATTLE_STATS[number];
/** A constant array comprised of {@linkcode Stat} the values that make up {@linkcode TempBattleStat}. */
export const TEMP_BATTLE_STATS = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD, Stat.ACC ] as const;
/** Type used to describe the stats that have X item (`TEMP_STAT_STAGE_BOOSTER`) equivalents. */
export type TempBattleStat = typeof TEMP_BATTLE_STATS[number];
/**
* Provides the translation key corresponding to the amount of stat stages and whether those stat stages
* are positive or negative.
* @param stages the amount of stages
* @param isIncrease dictates a negative (`false`) or a positive (`true`) stat stage change
* @returns the translation key fitting the conditions described by {@linkcode stages} and {@linkcode isIncrease}
*/
export function getStatStageChangeDescriptionKey(stages: number, isIncrease: boolean) {
if (stages === 1) {
return isIncrease ? "battle:statRose" : "battle:statFell";
} else if (stages === 2) {
return isIncrease ? "battle:statSharplyRose" : "battle:statHarshlyFell";
} else if (stages <= 6) {
return isIncrease ? "battle:statRoseDrastically" : "battle:statSeverelyFell";
}
return isIncrease ? "battle:statWontGoAnyHigher" : "battle:statWontGoAnyLower";
}
/**
* Provides the translation key corresponding to a given stat which can be translated into its full name.
* @param stat the {@linkcode Stat} to be translated
* @returns the translation key corresponding to the given {@linkcode Stat}
*/
export function getStatKey(stat: Stat) {
return `pokemonInfo:Stat.${Stat[stat]}`;
}
/**
* Provides the translation key corresponding to a given stat which can be translated into its shortened name.
* @param stat the {@linkcode Stat} to be translated
* @returns the translation key corresponding to the given {@linkcode Stat}
*/
export function getShortenedStatKey(stat: PermanentStat) {
return `pokemonInfo:Stat.${Stat[stat]}shortened`;
} }

View File

@ -3,26 +3,24 @@ 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, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr } from "../data/move"; import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr } from "../data/move";
import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species"; import { 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";
import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "../data/type"; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "../data/type";
import { getLevelTotalExp } from "../data/exp"; import { getLevelTotalExp } from "../data/exp";
import { Stat } from "../data/pokemon-stat"; import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, PokemonBaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempBattleStatBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier } from "../modifier/modifier"; 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 { PokeballType } from "../data/pokeball";
import { Gender } from "../data/gender"; import { Gender } from "../data/gender";
import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims"; import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims";
import { Status, StatusEffect, getRandomStatus } from "../data/status-effect"; import { Status, StatusEffect, getRandomStatus } from "../data/status-effect";
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions";
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
import { BattleStat } from "../data/battle-stat";
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag } from "../data/battler-tags"; import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag } from "../data/battler-tags";
import { WeatherType } from "../data/weather"; import { WeatherType } from "../data/weather";
import { TempBattleStat } from "../data/temp-battle-stat";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability"; import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability";
import PokemonData from "../system/pokemon-data"; import PokemonData from "../system/pokemon-data";
import { BattlerIndex } from "../battle"; import { BattlerIndex } from "../battle";
import { Mode } from "../ui/ui"; import { Mode } from "../ui/ui";
@ -40,7 +38,7 @@ import Overrides from "#app/overrides";
import i18next from "i18next"; import i18next from "i18next";
import { speciesEggMoves } from "../data/egg-moves"; import { speciesEggMoves } from "../data/egg-moves";
import { ModifierTier } from "../modifier/modifier-tier"; import { ModifierTier } from "../modifier/modifier-tier";
import { applyChallenges, ChallengeType } from "#app/data/challenge.js"; import { applyChallenges, ChallengeType } from "#app/data/challenge";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { BattleSpec } from "#enums/battle-spec"; import { BattleSpec } from "#enums/battle-spec";
@ -49,17 +47,17 @@ import { BerryType } from "#enums/berry-type";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { getPokemonNameWithAffix } from "#app/messages";
import { DamagePhase } from "#app/phases/damage-phase";
import { FaintPhase } from "#app/phases/faint-phase";
import { LearnMovePhase } from "#app/phases/learn-move-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { DamagePhase } from "#app/phases/damage-phase.js";
import { FaintPhase } from "#app/phases/faint-phase.js";
import { LearnMovePhase } from "#app/phases/learn-move-phase.js";
import { MoveEffectPhase } from "#app/phases/move-effect-phase.js";
import { MoveEndPhase } from "#app/phases/move-end-phase.js";
import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase.js";
import { StatChangePhase } from "#app/phases/stat-change-phase.js";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase.js";
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase.js";
export enum FieldPosition { export enum FieldPosition {
CENTER, CENTER,
@ -676,49 +674,139 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}); });
} }
getStat(stat: Stat): integer { /**
* Retrieves the entire set of stats of the {@linkcode Pokemon}.
* @param bypassSummonData prefer actual stats (`true` by default) or in-battle overriden stats (`false`)
* @returns the numeric values of the {@linkcode Pokemon}'s stats
*/
getStats(bypassSummonData: boolean = true): number[] {
if (!bypassSummonData && this.summonData?.stats) {
return this.summonData.stats;
}
return this.stats;
}
/**
* Retrieves the corresponding {@linkcode PermanentStat} of the {@linkcode Pokemon}.
* @param stat the desired {@linkcode PermanentStat}
* @param bypassSummonData prefer actual stats (`true` by default) or in-battle overridden stats (`false`)
* @returns the numeric value of the desired {@linkcode Stat}
*/
getStat(stat: PermanentStat, bypassSummonData: boolean = true): number {
if (!bypassSummonData && this.summonData && (this.summonData.stats[stat] !== 0)) {
return this.summonData.stats[stat];
}
return this.stats[stat]; return this.stats[stat];
} }
getBattleStat(stat: Stat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): integer { /**
if (stat === Stat.HP) { * Writes the value to the corrseponding {@linkcode PermanentStat} of the {@linkcode Pokemon}.
return this.getStat(Stat.HP); *
} * Note that this does nothing if {@linkcode value} is less than 0.
const battleStat = (stat - 1) as BattleStat; * @param stat the desired {@linkcode PermanentStat} to be overwritten
const statLevel = new Utils.IntegerHolder(this.summonData.battleStats[battleStat]); * @param value the desired numeric value
if (opponent) { * @param bypassSummonData write to actual stats (`true` by default) or in-battle overridden stats (`false`)
if (isCritical) { */
switch (stat) { setStat(stat: PermanentStat, value: number, bypassSummonData: boolean = true): void {
case Stat.ATK: if (value >= 0) {
case Stat.SPATK: if (!bypassSummonData && this.summonData) {
statLevel.value = Math.max(statLevel.value, 0); this.summonData.stats[stat] = value;
break; } else {
case Stat.DEF: this.stats[stat] = value;
case Stat.SPDEF:
statLevel.value = Math.min(statLevel.value, 0);
break;
}
}
applyAbAttrs(IgnoreOpponentStatChangesAbAttr, opponent, null, false, statLevel);
if (move) {
applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, opponent, move, statLevel);
} }
} }
if (this.isPlayer()) { }
this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), battleStat as integer as TempBattleStat, statLevel);
/**
* Retrieves the entire set of in-battle stat stages of the {@linkcode Pokemon}.
* @returns the numeric values of the {@linkcode Pokemon}'s in-battle stat stages if available, a fresh stat stage array otherwise
*/
getStatStages(): number[] {
return this.summonData ? this.summonData.statStages : [ 0, 0, 0, 0, 0, 0, 0 ];
}
/**
* Retrieves the in-battle stage of the specified {@linkcode BattleStat}.
* @param stat the {@linkcode BattleStat} whose stage is desired
* @returns the stage of the desired {@linkcode BattleStat} if available, 0 otherwise
*/
getStatStage(stat: BattleStat): number {
return this.summonData ? this.summonData.statStages[stat - 1] : 0;
}
/**
* Writes the value to the in-battle stage of the corresponding {@linkcode BattleStat} of the {@linkcode Pokemon}.
*
* Note that, if the value is not within a range of [-6, 6], it will be forced to the closest range bound.
* @param stat the {@linkcode BattleStat} whose stage is to be overwritten
* @param value the desired numeric value
*/
setStatStage(stat: BattleStat, value: number): void {
if (this.summonData) {
if (value >= -6) {
this.summonData.statStages[stat - 1] = Math.min(value, 6);
} else {
this.summonData.statStages[stat - 1] = Math.max(value, -6);
}
} }
const statValue = new Utils.NumberHolder(this.getStat(stat)); }
/**
* Retrieves the critical-hit stage considering the move used and the Pokemon
* who used it.
* @param source the {@linkcode Pokemon} who using the move
* @param move the {@linkcode Move} being used
* @returns the final critical-hit stage value
*/
getCritStage(source: Pokemon, move: Move): number {
const critStage = new Utils.IntegerHolder(0);
applyMoveAttrs(HighCritAttr, source, this, move, critStage);
this.scene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage);
this.scene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage);
const bonusCrit = new Utils.BooleanHolder(false);
//@ts-ignore
if (applyAbAttrs(BonusCritAbAttr, source, null, false, bonusCrit)) { // TODO: resolve ts-ignore. This is a promise. Checking a promise is bogus.
if (bonusCrit.value) {
critStage.value += 1;
}
}
const critBoostTag = source.getTag(CritBoostTag);
if (critBoostTag) {
if (critBoostTag instanceof DragonCheerTag) {
critStage.value += critBoostTag.typesOnAdd.includes(Type.DRAGON) ? 2 : 1;
} else {
critStage.value += 2;
}
}
console.log(`crit stage: +${critStage.value}`);
return critStage.value;
}
/**
* Calculates and retrieves the final value of a stat considering any held
* items, move effects, opponent abilities, and whether there was a critical
* hit.
* @param stat the desired {@linkcode EffectiveStat}
* @param opponent the target {@linkcode Pokemon}
* @param move the {@linkcode Move} being used
* @param isCritical determines whether a critical hit has occurred or not (`false` by default)
* @returns the final in-battle value of a stat
*/
getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): integer {
const statValue = new Utils.NumberHolder(this.getStat(stat, false));
this.scene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue); this.scene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue);
const fieldApplied = new Utils.BooleanHolder(false); const fieldApplied = new Utils.BooleanHolder(false);
for (const pokemon of this.scene.getField(true)) { 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) { if (fieldApplied.value) {
break; break;
} }
} }
applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, battleStat, statValue); applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, stat, statValue);
let ret = statValue.value * (Math.max(2, 2 + statLevel.value) / Math.max(2, 2 - statLevel.value)); let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, isCritical);
switch (stat) { switch (stat) {
case Stat.ATK: case Stat.ATK:
if (this.getTag(BattlerTagType.SLOW_START)) { if (this.getTag(BattlerTagType.SLOW_START)) {
@ -765,24 +853,25 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (!this.stats) { if (!this.stats) {
this.stats = [ 0, 0, 0, 0, 0, 0 ]; this.stats = [ 0, 0, 0, 0, 0, 0 ];
} }
const baseStats = this.getSpeciesForm().baseStats.slice(0);
if (this.fusionSpecies) { // Get and manipulate base stats
const fusionBaseStats = this.getFusionSpeciesForm().baseStats; const baseStats = this.getSpeciesForm(true).baseStats.slice();
for (let s = 0; s < this.stats.length; s++) { if (this.isFusion()) {
const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats;
for (const s of PERMANENT_STATS) {
baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2); baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2);
} }
} else if (this.scene.gameMode.isSplicedOnly) { } else if (this.scene.gameMode.isSplicedOnly) {
for (let s = 0; s < this.stats.length; s++) { for (const s of PERMANENT_STATS) {
baseStats[s] = Math.ceil(baseStats[s] / 2); baseStats[s] = Math.ceil(baseStats[s] / 2);
} }
} }
this.scene.applyModifiers(PokemonBaseStatModifier, this.isPlayer(), this, baseStats); this.scene.applyModifiers(BaseStatModifier, this.isPlayer(), this, baseStats);
const stats = Utils.getEnumValues(Stat);
for (const s of stats) { // Using base stats, calculate and store stats one by one
const isHp = s === Stat.HP; for (const s of PERMANENT_STATS) {
const baseStat = baseStats[s]; let value = Math.floor(((2 * baseStats[s] + this.ivs[s]) * this.level) * 0.01);
let value = Math.floor(((2 * baseStat + this.ivs[s]) * this.level) * 0.01); if (s === Stat.HP) {
if (isHp) {
value = value + this.level + 10; value = value + this.level + 10;
if (this.hasAbility(Abilities.WONDER_GUARD, false, true)) { if (this.hasAbility(Abilities.WONDER_GUARD, false, true)) {
value = 1; value = 1;
@ -803,7 +892,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
value = Math.max(Math[natureStatMultiplier.value > 1 ? "ceil" : "floor"](value * natureStatMultiplier.value), 1); value = Math.max(Math[natureStatMultiplier.value > 1 ? "ceil" : "floor"](value * natureStatMultiplier.value), 1);
} }
} }
this.stats[s] = value;
this.setStat(s, value);
} }
} }
@ -1378,7 +1468,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const types = this.getTypes(true); const types = this.getTypes(true);
const enemyTypes = opponent.getTypes(true, true); const enemyTypes = opponent.getTypes(true, true);
/** Is this Pokemon faster than the opponent? */ /** 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. * 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. * This score is increased by 25 percent if this Pokemon is faster than the opponent.
@ -1757,7 +1847,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(SacrificialAttr) ? 0.5 : 1)]); movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(SacrificialAttr) ? 0.5 : 1)]);
movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(SacrificialAttrOnHit) ? 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 // 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 // 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)]); movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(ChargeAttr) || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1)]);
} }
@ -1769,8 +1859,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === MoveCategory.STATUS ? 1 : Math.max(Math.min(allMoves[m[0]].power/maxPower, 1), 0.5))]); movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === MoveCategory.STATUS ? 1 : Math.max(Math.min(allMoves[m[0]].power/maxPower, 1), 0.5))]);
// Weight damaging moves against the lower stat // Weight damaging moves against the lower stat
const worseCategory: MoveCategory = this.stats[Stat.ATK] > this.stats[Stat.SPATK] ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL; const atk = this.getStat(Stat.ATK);
const statRatio = worseCategory === MoveCategory.PHYSICAL ? this.stats[Stat.ATK]/this.stats[Stat.SPATK] : this.stats[Stat.SPATK]/this.stats[Stat.ATK]; const spAtk = this.getStat(Stat.SPATK);
const worseCategory: MoveCategory = atk > spAtk ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL;
const statRatio = worseCategory === MoveCategory.PHYSICAL ? atk / spAtk : spAtk / atk;
movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === worseCategory ? statRatio : 1)]); movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === worseCategory ? statRatio : 1)]);
let weightMultiplier = 0.9; // The higher this is the more the game weights towards higher level moves. At 0 all moves are equal weight. let weightMultiplier = 0.9; // The higher this is the more the game weights towards higher level moves. At 0 all moves are equal weight.
@ -1956,6 +2048,48 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return this instanceof PlayerPokemon ? this.scene.getPlayerField() : this.scene.getEnemyField(); return this instanceof PlayerPokemon ? this.scene.getPlayerField() : this.scene.getEnemyField();
} }
/**
* Calculates the stat stage multiplier of the user against an opponent.
*
* Note that this does not apply to evasion or accuracy
* @see {@linkcode getAccuracyMultiplier}
* @param stat the desired {@linkcode EffectiveStat}
* @param opponent the target {@linkcode Pokemon}
* @param move the {@linkcode Move} being used
* @param isCritical determines whether a critical hit has occurred or not (`false` by default)
* @return the stat stage multiplier to be used for effective stat calculation
*/
getStatStageMultiplier(stat: EffectiveStat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): number {
const statStage = new Utils.IntegerHolder(this.getStatStage(stat));
const ignoreStatStage = new Utils.BooleanHolder(false);
if (opponent) {
if (isCritical) {
switch (stat) {
case Stat.ATK:
case Stat.SPATK:
statStage.value = Math.max(statStage.value, 0);
break;
case Stat.DEF:
case Stat.SPDEF:
statStage.value = Math.min(statStage.value, 0);
break;
}
}
applyAbAttrs(IgnoreOpponentStatStagesAbAttr, opponent, null, false, stat, ignoreStatStage);
if (move) {
applyMoveAttrs(IgnoreOpponentStatStagesAttr, this, opponent, move, ignoreStatStage);
}
}
if (!ignoreStatStage.value) {
const statStageMultiplier = new Utils.NumberHolder(Math.max(2, 2 + statStage.value) / Math.max(2, 2 - statStage.value));
this.scene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier);
return Math.min(statStageMultiplier.value, 4);
}
return 1;
}
/** /**
* Calculates the accuracy multiplier of the user against a target. * Calculates the accuracy multiplier of the user against a target.
* *
@ -1972,34 +2106,38 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return 1; return 1;
} }
const userAccuracyLevel = new Utils.IntegerHolder(this.summonData.battleStats[BattleStat.ACC]); const userAccStage = new Utils.IntegerHolder(this.getStatStage(Stat.ACC));
const targetEvasionLevel = new Utils.IntegerHolder(target.summonData.battleStats[BattleStat.EVA]); const targetEvaStage = new Utils.IntegerHolder(target.getStatStage(Stat.EVA));
applyAbAttrs(IgnoreOpponentStatChangesAbAttr, target, null, false, userAccuracyLevel); const ignoreAccStatStage = new Utils.BooleanHolder(false);
applyAbAttrs(IgnoreOpponentStatChangesAbAttr, this, null, false, targetEvasionLevel); const ignoreEvaStatStage = new Utils.BooleanHolder(false);
applyAbAttrs(IgnoreOpponentEvasionAbAttr, this, null, false, targetEvasionLevel);
applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, target, sourceMove, targetEvasionLevel); applyAbAttrs(IgnoreOpponentStatStagesAbAttr, target, null, false, Stat.ACC, ignoreAccStatStage);
this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), TempBattleStat.ACC, userAccuracyLevel); applyAbAttrs(IgnoreOpponentStatStagesAbAttr, this, null, false, Stat.EVA, ignoreEvaStatStage);
applyMoveAttrs(IgnoreOpponentStatStagesAttr, this, target, sourceMove, ignoreEvaStatStage);
this.scene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), Stat.ACC, userAccStage);
userAccStage.value = ignoreAccStatStage.value ? 0 : Math.min(userAccStage.value, 6);
targetEvaStage.value = ignoreEvaStatStage.value ? 0 : targetEvaStage.value;
if (target.findTag(t => t instanceof ExposedTag)) { if (target.findTag(t => t instanceof ExposedTag)) {
targetEvasionLevel.value = Math.min(0, targetEvasionLevel.value); targetEvaStage.value = Math.min(0, targetEvaStage.value);
} }
const accuracyMultiplier = new Utils.NumberHolder(1); const accuracyMultiplier = new Utils.NumberHolder(1);
if (userAccuracyLevel.value !== targetEvasionLevel.value) { if (userAccStage.value !== targetEvaStage.value) {
accuracyMultiplier.value = userAccuracyLevel.value > targetEvasionLevel.value accuracyMultiplier.value = userAccStage.value > targetEvaStage.value
? (3 + Math.min(userAccuracyLevel.value - targetEvasionLevel.value, 6)) / 3 ? (3 + Math.min(userAccStage.value - targetEvaStage.value, 6)) / 3
: 3 / (3 + Math.min(targetEvasionLevel.value - userAccuracyLevel.value, 6)); : 3 / (3 + Math.min(targetEvaStage.value - userAccStage.value, 6));
} }
applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, BattleStat.ACC, accuracyMultiplier, false, sourceMove); applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, Stat.ACC, accuracyMultiplier, false, sourceMove);
const evasionMultiplier = new Utils.NumberHolder(1); const evasionMultiplier = new Utils.NumberHolder(1);
applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, target, BattleStat.EVA, evasionMultiplier); applyStatMultiplierAbAttrs(StatMultiplierAbAttr, target, Stat.EVA, evasionMultiplier);
accuracyMultiplier.value /= evasionMultiplier.value; return accuracyMultiplier.value / evasionMultiplier.value;
return accuracyMultiplier.value;
} }
/** /**
@ -2086,29 +2224,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (critOnly.value || critAlways) { if (critOnly.value || critAlways) {
isCritical = true; isCritical = true;
} else { } else {
const critLevel = new Utils.IntegerHolder(0); const critChance = [24, 8, 2, 1][Math.max(0, Math.min(this.getCritStage(source, move), 3))];
applyMoveAttrs(HighCritAttr, source, this, move, critLevel);
this.scene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critLevel);
this.scene.applyModifiers(TempBattleStatBoosterModifier, source.isPlayer(), TempBattleStat.CRIT, critLevel);
const bonusCrit = new Utils.BooleanHolder(false);
//@ts-ignore
if (applyAbAttrs(BonusCritAbAttr, source, null, false, bonusCrit)) { // TODO: resolve ts-ignore. This is a promise. Checking a promise is bogus.
if (bonusCrit.value) {
critLevel.value += 1;
}
}
const critBoostTag = source.getTag(CritBoostTag);
if (critBoostTag) {
if (critBoostTag instanceof DragonCheerTag) {
critLevel.value += critBoostTag.typesOnAdd.includes(Type.DRAGON) ? 2 : 1;
} else {
critLevel.value += 2;
}
}
console.log(`crit stage: +${critLevel.value}`);
const critChance = [24, 8, 2, 1][Math.max(0, Math.min(critLevel.value, 3))];
isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance); isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance);
if (Overrides.NEVER_CRIT_OVERRIDE) { if (Overrides.NEVER_CRIT_OVERRIDE) {
isCritical = false; isCritical = false;
@ -2122,8 +2238,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
isCritical = false; isCritical = false;
} }
} }
const sourceAtk = new Utils.IntegerHolder(source.getBattleStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, isCritical)); const sourceAtk = new Utils.IntegerHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, isCritical));
const targetDef = new Utils.IntegerHolder(this.getBattleStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, 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); const criticalMultiplier = new Utils.NumberHolder(isCritical ? 1.5 : 1);
applyAbAttrs(MultCritAbAttr, source, null, false, criticalMultiplier); applyAbAttrs(MultCritAbAttr, source, null, false, criticalMultiplier);
const screenMultiplier = new Utils.NumberHolder(1); const screenMultiplier = new Utils.NumberHolder(1);
@ -2534,10 +2650,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param source {@linkcode Pokemon} the pokemon whose stats/Tags are to be passed on from, ie: the Pokemon using Baton Pass * @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 { transferSummon(source: Pokemon): void {
const battleStats = Utils.getEnumValues(BattleStat); // Copy all stat stages
for (const stat of battleStats) { for (const s of BATTLE_STATS) {
this.summonData.battleStats[stat] = source.summonData.battleStats[stat]; 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) { for (const tag of source.summonData.tags) {
// bypass those can not be passed via Baton Pass // bypass those can not be passed via Baton Pass
@ -2549,9 +2670,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.summonData.tags.push(tag); this.summonData.tags.push(tag);
} }
if (this instanceof PlayerPokemon && source.summonData.battleStats.find(bs => bs === 6)) {
this.scene.validateAchv(achvs.TRANSFER_MAX_BATTLE_STAT);
}
this.updateInfo(); this.updateInfo();
} }
@ -3729,16 +3848,17 @@ export class PlayerPokemon extends Pokemon {
this.scene.gameData.gameStats.pokemonFused++; this.scene.gameData.gameStats.pokemonFused++;
// Store the average HP% that each Pokemon has // Store the average HP% that each Pokemon has
const newHpPercent = ((pokemon.hp / pokemon.stats[Stat.HP]) + (this.hp / this.stats[Stat.HP])) / 2; const maxHp = this.getMaxHp();
const newHpPercent = ((pokemon.hp / pokemon.getMaxHp()) + (this.hp / maxHp)) / 2;
this.generateName(); this.generateName();
this.calculateStats(); this.calculateStats();
// Set this Pokemon's HP to the average % of both fusion components // Set this Pokemon's HP to the average % of both fusion components
this.hp = Math.round(this.stats[Stat.HP] * newHpPercent); this.hp = Math.round(maxHp * newHpPercent);
if (!this.isFainted()) { if (!this.isFainted()) {
// If this Pokemon hasn't fainted, make sure the HP wasn't set over the new maximum // If this Pokemon hasn't fainted, make sure the HP wasn't set over the new maximum
this.hp = Math.min(this.hp, this.stats[Stat.HP]); this.hp = Math.min(this.hp, maxHp);
this.status = getRandomStatus(this.status, pokemon.status); // Get a random valid status between the two this.status = getRandomStatus(this.status, pokemon.status); // Get a random valid status between the two
} else if (!pokemon.isFainted()) { } else if (!pokemon.isFainted()) {
// If this Pokemon fainted but the other hasn't, make sure the HP wasn't set to zero // If this Pokemon fainted but the other hasn't, make sure the HP wasn't set to zero
@ -4231,39 +4351,40 @@ export class EnemyPokemon extends Pokemon {
handleBossSegmentCleared(segmentIndex: integer): void { handleBossSegmentCleared(segmentIndex: integer): void {
while (segmentIndex - 1 < this.bossSegmentIndex) { 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); let boostedStat: EffectiveStat;
const statWeights = new Array().fill(battleStats.length).filter((bs: BattleStat) => this.summonData.battleStats[bs] < 6).map((bs: BattleStat) => this.getStat(bs + 1)); const statThresholds: number[] = [];
const statThresholds: integer[] = [];
let totalWeight = 0; let totalWeight = 0;
for (const bs of battleStats) {
totalWeight += statWeights[bs]; for (const i in statWeights) {
totalWeight += statWeights[i];
statThresholds.push(totalWeight); statThresholds.push(totalWeight);
} }
// Pick a random stat from the leftover stats to increase its stages
const randInt = Utils.randSeedInt(totalWeight); const randInt = Utils.randSeedInt(totalWeight);
for (const i in statThresholds) {
for (const bs of battleStats) { if (randInt < statThresholds[i]) {
if (randInt < statThresholds[bs]) { boostedStat = leftoverStats[i];
boostedStat = bs;
break; break;
} }
} }
let statLevels = 1; let stages = 1;
// increase the boost if the boss has at least 3 segments and we passed last shield // increase the boost if the boss has at least 3 segments and we passed last shield
if (this.bossSegments >= 3 && this.bossSegmentIndex === 1) { if (this.bossSegments >= 3 && this.bossSegmentIndex === 1) {
statLevels++; stages++;
} }
// increase the boost if the boss has at least 5 segments and we passed the second to last shield // increase the boost if the boss has at least 5 segments and we passed the second to last shield
if (this.bossSegments >= 5 && this.bossSegmentIndex === 2) { if (this.bossSegments >= 5 && this.bossSegmentIndex === 2) {
statLevels++; stages++;
} }
this.scene.unshiftPhase(new StatChangePhase(this.scene, this.getBattlerIndex(), true, [ boostedStat ], statLevels, true, true)); this.scene.unshiftPhase(new StatStageChangePhase(this.scene, this.getBattlerIndex(), true, [ boostedStat! ], stages, true, true));
this.bossSegmentIndex--; this.bossSegmentIndex--;
} }
} }
@ -4339,7 +4460,7 @@ export interface AttackMoveResult {
} }
export class PokemonSummonData { export class PokemonSummonData {
public battleStats: number[] = [ 0, 0, 0, 0, 0, 0, 0 ]; public statStages: number[] = [ 0, 0, 0, 0, 0, 0, 0 ];
public moveQueue: QueuedMove[] = []; public moveQueue: QueuedMove[] = [];
public disabledMove: Moves = Moves.NONE; public disabledMove: Moves = Moves.NONE;
public disabledTurns: number = 0; public disabledTurns: number = 0;
@ -4352,7 +4473,7 @@ export class PokemonSummonData {
public ability: Abilities = Abilities.NONE; public ability: Abilities = Abilities.NONE;
public gender: Gender; public gender: Gender;
public fusionGender: Gender; public fusionGender: Gender;
public stats: number[]; public stats: number[] = [ 0, 0, 0, 0, 0, 0 ];
public moveset: (PokemonMove | null)[]; public moveset: (PokemonMove | null)[];
// If not initialized this value will not be populated from save data. // If not initialized this value will not be populated from save data.
public types: Type[] = []; public types: Type[] = [];
@ -4383,8 +4504,8 @@ export class PokemonTurnData {
public damageTaken: number = 0; public damageTaken: number = 0;
public attacksReceived: AttackMoveResult[] = []; public attacksReceived: AttackMoveResult[] = [];
public order: number; public order: number;
public battleStatsIncreased: boolean = false; public statStagesIncreased: boolean = false;
public battleStatsDecreased: boolean = false; public statStagesDecreased: boolean = false;
} }
export enum AiType { export enum AiType {

View File

@ -37,8 +37,7 @@ export interface ModifierTypeTranslationEntries {
ModifierType: { [key: string]: ModifierTypeTranslationEntry }, ModifierType: { [key: string]: ModifierTypeTranslationEntry },
SpeciesBoosterItem: { [key: string]: ModifierTypeTranslationEntry }, SpeciesBoosterItem: { [key: string]: ModifierTypeTranslationEntry },
AttackTypeBoosterItem: SimpleTranslationEntries, AttackTypeBoosterItem: SimpleTranslationEntries,
TempBattleStatBoosterItem: SimpleTranslationEntries, TempStatStageBoosterItem: SimpleTranslationEntries,
TempBattleStatBoosterStatName: SimpleTranslationEntries,
BaseStatBoosterItem: SimpleTranslationEntries, BaseStatBoosterItem: SimpleTranslationEntries,
EvolutionItem: SimpleTranslationEntries, EvolutionItem: SimpleTranslationEntries,
FormChangeItem: SimpleTranslationEntries, FormChangeItem: SimpleTranslationEntries,

View File

@ -89,7 +89,7 @@
"name": "Bänder-Meister", "name": "Bänder-Meister",
"name_female": "Bänder-Meisterin" "name_female": "Bänder-Meisterin"
}, },
"TRANSFER_MAX_BATTLE_STAT": { "TRANSFER_MAX_STAT_STAGE": {
"name": "Teamwork", "name": "Teamwork",
"description": "Nutze Staffette, während der Anwender mindestens eines Statuswertes maximiert hat." "description": "Nutze Staffette, während der Anwender mindestens eines Statuswertes maximiert hat."
}, },
@ -274,4 +274,4 @@
"name": "Spieglein, Spieglein an der Wand", "name": "Spieglein, Spieglein an der Wand",
"description": "Schließe die 'Umkehrkampf' Herausforderung ab" "description": "Schließe die 'Umkehrkampf' Herausforderung ab"
} }
} }

View File

@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": { "DoubleBattleChanceBoosterModifierType": {
"description": "Verdoppelt die Wahrscheinlichkeit, dass die nächsten {{battleCount}} Begegnungen mit wilden Pokémon ein Doppelkampf sind." "description": "Verdoppelt die Wahrscheinlichkeit, dass die nächsten {{battleCount}} Begegnungen mit wilden Pokémon ein Doppelkampf sind."
}, },
"TempBattleStatBoosterModifierType": { "TempStatStageBoosterModifierType": {
"description": "Erhöht die {{tempBattleStatName}} aller Teammitglieder für 5 Kämpfe um eine Stufe." "description": "Erhöht die {{stat}} aller Teammitglieder für 5 Kämpfe um eine Stufe."
}, },
"AttackTypeBoosterModifierType": { "AttackTypeBoosterModifierType": {
"description": "Erhöht die Stärke aller {{moveType}}-Attacken eines Pokémon um 20%." "description": "Erhöht die Stärke aller {{moveType}}-Attacken eines Pokémon um 20%."
@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": { "AllPokemonLevelIncrementModifierType": {
"description": "Erhöht das Level aller Teammitglieder um {{levels}}." "description": "Erhöht das Level aller Teammitglieder um {{levels}}."
}, },
"PokemonBaseStatBoosterModifierType": { "BaseStatBoosterModifierType": {
"description": "Erhöht den {{statName}} Basiswert des Trägers um 10%. Das Stapellimit erhöht sich, je höher dein IS-Wert ist." "description": "Erhöht den {{stat}} Basiswert des Trägers um 10%. Das Stapellimit erhöht sich, je höher dein IS-Wert ist."
}, },
"AllPokemonFullHpRestoreModifierType": { "AllPokemonFullHpRestoreModifierType": {
"description": "Stellt 100% der KP aller Pokémon her." "description": "Stellt 100% der KP aller Pokémon her."
@ -248,6 +248,12 @@
"name": "Scope-Linse", "name": "Scope-Linse",
"description": "Ein Item zum Tragen. Es erhöht die Volltrefferquote." "description": "Ein Item zum Tragen. Es erhöht die Volltrefferquote."
}, },
"DIRE_HIT": {
"name": "X-Volltreffer",
"extra": {
"raises": "Volltrefferquote"
}
},
"LEEK": { "LEEK": {
"name": "Lauchstange", "name": "Lauchstange",
"description": "Ein Item, das von Porenta getragen werden kann. Diese lange Lauchstange erhöht die Volltrefferquote stark." "description": "Ein Item, das von Porenta getragen werden kann. Diese lange Lauchstange erhöht die Volltrefferquote stark."
@ -411,25 +417,13 @@
"description": "Ein Item, das Ditto zum Tragen gegeben werden kann. Fein und doch hart, erhöht dieses sonderbare Pulver die Initiative." "description": "Ein Item, das Ditto zum Tragen gegeben werden kann. Fein und doch hart, erhöht dieses sonderbare Pulver die Initiative."
} }
}, },
"TempBattleStatBoosterItem": { "TempStatStageBoosterItem": {
"x_attack": "X-Angriff", "x_attack": "X-Angriff",
"x_defense": "X-Verteidigung", "x_defense": "X-Verteidigung",
"x_sp_atk": "X-Sp.-Ang.", "x_sp_atk": "X-Sp.-Ang.",
"x_sp_def": "X-Sp.-Vert.", "x_sp_def": "X-Sp.-Vert.",
"x_speed": "X-Tempo", "x_speed": "X-Tempo",
"x_accuracy": "X-Treffer", "x_accuracy": "X-Treffer"
"dire_hit": "X-Volltreffer"
},
"TempBattleStatBoosterStatName": {
"ATK": "Angriff",
"DEF": "Verteidigung",
"SPATK": "Sp. Ang",
"SPDEF": "Sp. Vert",
"SPD": "Initiative",
"ACC": "Genauigkeit",
"CRIT": "Volltrefferquote",
"EVA": "Fluchtwert",
"DEFAULT": "???"
}, },
"AttackTypeBoosterItem": { "AttackTypeBoosterItem": {
"silk_scarf": "Seidenschal", "silk_scarf": "Seidenschal",
@ -606,4 +600,4 @@
"FAIRY_MEMORY": "Feen-Disc", "FAIRY_MEMORY": "Feen-Disc",
"NORMAL_MEMORY": "Normal-Disc" "NORMAL_MEMORY": "Normal-Disc"
} }
} }

View File

@ -3,7 +3,7 @@
"turnHealApply": "{{typeName}} von {{pokemonNameWithAffix}} füllt einige KP auf!", "turnHealApply": "{{typeName}} von {{pokemonNameWithAffix}} füllt einige KP auf!",
"hitHealApply": "{{typeName}} von {{pokemonNameWithAffix}} füllt einige KP auf!", "hitHealApply": "{{typeName}} von {{pokemonNameWithAffix}} füllt einige KP auf!",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}} wurde durch {{typeName}} wiederbelebt!", "pokemonInstantReviveApply": "{{pokemonNameWithAffix}} wurde durch {{typeName}} wiederbelebt!",
"pokemonResetNegativeStatStageApply": "Die negative Statuswertveränderung von {{pokemonNameWithAffix}} wurde durch {{typeName}} aufgehoben!", "resetNegativeStatStageApply": "Die negative Statuswertveränderung von {{pokemonNameWithAffix}} wurde durch {{typeName}} aufgehoben!",
"moneyInterestApply": "Du erhählst {{moneyAmount}} ₽ durch das Item {{typeName}}!", "moneyInterestApply": "Du erhählst {{moneyAmount}} ₽ durch das Item {{typeName}}!",
"turnHeldItemTransferApply": "{{itemName}} von {{pokemonNameWithAffix}} wurde durch {{typeName}} von {{pokemonName}} absorbiert!", "turnHeldItemTransferApply": "{{itemName}} von {{pokemonNameWithAffix}} wurde durch {{typeName}} von {{pokemonName}} absorbiert!",
"contactHeldItemTransferApply": "{{itemName}} von {{pokemonNameWithAffix}} wurde durch {{typeName}} von {{pokemonName}} geklaut!", "contactHeldItemTransferApply": "{{itemName}} von {{pokemonNameWithAffix}} wurde durch {{typeName}} von {{pokemonName}} geklaut!",

View File

@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}} nutzt seine KP um seine Attacke zu verstärken!", "cutHpPowerUpMove": "{{pokemonName}} nutzt seine KP um seine Attacke zu verstärken!",
"absorbedElectricity": "{{pokemonName}} absorbiert elektrische Energie!", "absorbedElectricity": "{{pokemonName}} absorbiert elektrische Energie!",
"switchedStatChanges": "{{pokemonName}} tauschte die Statuswerteveränderungen mit dem Ziel!", "switchedStatChanges": "{{pokemonName}} tauschte die Statuswerteveränderungen mit dem Ziel!",
"switchedTwoStatChanges": "{{pokemonName}} tauscht Veränderungen an {{firstStat}} und {{secondStat}} mit dem Ziel!",
"switchedStat": "{{pokemonName}} tauscht seinen {{stat}}-Wert mit dem des Zieles!",
"sharedGuard": "{{pokemonName}} addiert seine Schutzkräfte mit jenen des Zieles und teilt sie gerecht auf!",
"sharedPower": "{{pokemonName}} addiert seine Kräfte mit jenen des Zieles und teilt sie gerecht auf!",
"goingAllOutForAttack": "{{pokemonName}} legt sich ins Zeug!", "goingAllOutForAttack": "{{pokemonName}} legt sich ins Zeug!",
"regainedHealth": "{{pokemonName}} erholt sich!", "regainedHealth": "{{pokemonName}} erholt sich!",
"keptGoingAndCrashed": "{{pokemonName}} springt daneben und verletzt sich!", "keptGoingAndCrashed": "{{pokemonName}} springt daneben und verletzt sich!",
@ -63,4 +67,4 @@
"swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!", "swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!",
"exposedMove": "{{pokemonName}} erkennt {{targetPokemonName}}!", "exposedMove": "{{pokemonName}} erkennt {{targetPokemonName}}!",
"safeguard": "{{targetName}} wird durch Bodyguard geschützt!" "safeguard": "{{targetName}} wird durch Bodyguard geschützt!"
} }

View File

@ -1,7 +1,6 @@
{ {
"Stat": { "Stat": {
"HP": "KP", "HP": "KP",
"HPStat": "KP",
"HPshortened": "KP", "HPshortened": "KP",
"ATK": "Angriff", "ATK": "Angriff",
"ATKshortened": "Ang", "ATKshortened": "Ang",

View File

@ -97,9 +97,9 @@
"name": "Master League Champion", "name": "Master League Champion",
"name_female": "Master League Champion" "name_female": "Master League Champion"
}, },
"TRANSFER_MAX_BATTLE_STAT": { "TRANSFER_MAX_STAT_STAGE": {
"name": "Teamwork", "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": { "MAX_FRIENDSHIP": {
"name": "Friendmaxxing", "name": "Friendmaxxing",
@ -284,4 +284,4 @@
"name": "Mirror rorriM", "name": "Mirror rorriM",
"description": "Complete the Inverse Battle challenge.\n.egnellahc elttaB esrevnI eht etelpmoC" "description": "Complete the Inverse Battle challenge.\n.egnellahc elttaB esrevnI eht etelpmoC"
} }
} }

View File

@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": { "DoubleBattleChanceBoosterModifierType": {
"description": "Doubles the chance of an encounter being a double battle for {{battleCount}} battles." "description": "Doubles the chance of an encounter being a double battle for {{battleCount}} battles."
}, },
"TempBattleStatBoosterModifierType": { "TempStatStageBoosterModifierType": {
"description": "Increases the {{tempBattleStatName}} of all party members by 1 stage for 5 battles." "description": "Increases the {{stat}} of all party members by 1 stage for 5 battles."
}, },
"AttackTypeBoosterModifierType": { "AttackTypeBoosterModifierType": {
"description": "Increases the power of a Pokémon's {{moveType}}-type moves by 20%." "description": "Increases the power of a Pokémon's {{moveType}}-type moves by 20%."
@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": { "AllPokemonLevelIncrementModifierType": {
"description": "Increases all party members' level by {{levels}}." "description": "Increases all party members' level by {{levels}}."
}, },
"PokemonBaseStatBoosterModifierType": { "BaseStatBoosterModifierType": {
"description": "Increases the holder's base {{statName}} by 10%. The higher your IVs, the higher the stack limit." "description": "Increases the holder's base {{stat}} by 10%. The higher your IVs, the higher the stack limit."
}, },
"AllPokemonFullHpRestoreModifierType": { "AllPokemonFullHpRestoreModifierType": {
"description": "Restores 100% HP for all Pokémon." "description": "Restores 100% HP for all Pokémon."
@ -183,6 +183,7 @@
"SOOTHE_BELL": { "name": "Soothe Bell" }, "SOOTHE_BELL": { "name": "Soothe Bell" },
"SCOPE_LENS": { "name": "Scope Lens", "description": "It's a lens for scoping out weak points. It boosts the holder's critical-hit ratio."}, "SCOPE_LENS": { "name": "Scope Lens", "description": "It's a lens for scoping out weak points. It boosts the holder's critical-hit ratio."},
"DIRE_HIT": { "name": "Dire Hit", "extra": { "raises": "Critical Hit Ratio" } },
"LEEK": { "name": "Leek", "description": "This very long and stiff stalk of leek boosts the critical-hit ratio of Farfetch'd's moves."}, "LEEK": { "name": "Leek", "description": "This very long and stiff stalk of leek boosts the critical-hit ratio of Farfetch'd's moves."},
"EVIOLITE": { "name": "Eviolite", "description": "This mysterious evolutionary lump boosts the Defense and Sp. Def stats when held by a Pokémon that can still evolve." }, "EVIOLITE": { "name": "Eviolite", "description": "This mysterious evolutionary lump boosts the Defense and Sp. Def stats when held by a Pokémon that can still evolve." },
@ -250,28 +251,14 @@
"METAL_POWDER": { "name": "Metal Powder", "description": "Extremely fine yet hard, this odd powder boosts Ditto's Defense stat." }, "METAL_POWDER": { "name": "Metal Powder", "description": "Extremely fine yet hard, this odd powder boosts Ditto's Defense stat." },
"QUICK_POWDER": { "name": "Quick Powder", "description": "Extremely fine yet hard, this odd powder boosts Ditto's Speed stat." } "QUICK_POWDER": { "name": "Quick Powder", "description": "Extremely fine yet hard, this odd powder boosts Ditto's Speed stat." }
}, },
"TempBattleStatBoosterItem": { "TempStatStageBoosterItem": {
"x_attack": "X Attack", "x_attack": "X Attack",
"x_defense": "X Defense", "x_defense": "X Defense",
"x_sp_atk": "X Sp. Atk", "x_sp_atk": "X Sp. Atk",
"x_sp_def": "X Sp. Def", "x_sp_def": "X Sp. Def",
"x_speed": "X Speed", "x_speed": "X Speed",
"x_accuracy": "X Accuracy", "x_accuracy": "X Accuracy"
"dire_hit": "Dire Hit"
}, },
"TempBattleStatBoosterStatName": {
"ATK": "Attack",
"DEF": "Defense",
"SPATK": "Sp. Atk",
"SPDEF": "Sp. Def",
"SPD": "Speed",
"ACC": "Accuracy",
"CRIT": "Critical Hit Ratio",
"EVA": "Evasiveness",
"DEFAULT": "???"
},
"AttackTypeBoosterItem": { "AttackTypeBoosterItem": {
"silk_scarf": "Silk Scarf", "silk_scarf": "Silk Scarf",
"black_belt": "Black Belt", "black_belt": "Black Belt",

View File

@ -3,7 +3,7 @@
"turnHealApply": "{{pokemonNameWithAffix}} restored a little HP using\nits {{typeName}}!", "turnHealApply": "{{pokemonNameWithAffix}} restored a little HP using\nits {{typeName}}!",
"hitHealApply": "{{pokemonNameWithAffix}} restored a little HP using\nits {{typeName}}!", "hitHealApply": "{{pokemonNameWithAffix}} restored a little HP using\nits {{typeName}}!",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}} was revived\nby its {{typeName}}!", "pokemonInstantReviveApply": "{{pokemonNameWithAffix}} was revived\nby its {{typeName}}!",
"pokemonResetNegativeStatStageApply": "{{pokemonNameWithAffix}}'s lowered stats were restored\nby its {{typeName}}!", "resetNegativeStatStageApply": "{{pokemonNameWithAffix}}'s lowered stats were restored\nby its {{typeName}}!",
"moneyInterestApply": "You received interest of ₽{{moneyAmount}}\nfrom the {{typeName}}!", "moneyInterestApply": "You received interest of ₽{{moneyAmount}}\nfrom the {{typeName}}!",
"turnHeldItemTransferApply": "{{pokemonNameWithAffix}}'s {{itemName}} was absorbed\nby {{pokemonName}}'s {{typeName}}!", "turnHeldItemTransferApply": "{{pokemonNameWithAffix}}'s {{itemName}} was absorbed\nby {{pokemonName}}'s {{typeName}}!",
"contactHeldItemTransferApply": "{{pokemonNameWithAffix}}'s {{itemName}} was snatched\nby {{pokemonName}}'s {{typeName}}!", "contactHeldItemTransferApply": "{{pokemonNameWithAffix}}'s {{itemName}} was snatched\nby {{pokemonName}}'s {{typeName}}!",

View File

@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}} cut its own HP to power up its move!", "cutHpPowerUpMove": "{{pokemonName}} cut its own HP to power up its move!",
"absorbedElectricity": "{{pokemonName}} absorbed electricity!", "absorbedElectricity": "{{pokemonName}} absorbed electricity!",
"switchedStatChanges": "{{pokemonName}} switched stat changes with the target!", "switchedStatChanges": "{{pokemonName}} switched stat changes with the target!",
"switchedTwoStatChanges": "{{pokemonName}} switched all changes to its {{firstStat}}\nand {{secondStat}} with its target!",
"switchedStat": "{{pokemonName}} switched {{stat}} with its target!",
"sharedGuard": "{{pokemonName}} shared its guard with the target!",
"sharedPower": "{{pokemonName}} shared its power with the target!",
"goingAllOutForAttack": "{{pokemonName}} is going all out for this attack!", "goingAllOutForAttack": "{{pokemonName}} is going all out for this attack!",
"regainedHealth": "{{pokemonName}} regained\nhealth!", "regainedHealth": "{{pokemonName}} regained\nhealth!",
"keptGoingAndCrashed": "{{pokemonName}} kept going\nand crashed!", "keptGoingAndCrashed": "{{pokemonName}} kept going\nand crashed!",

View File

@ -1,7 +1,7 @@
{ {
"Stat": { "Stat": {
"HP": "Max. HP", "HP": "Max. HP",
"HPshortened": "MaxHP", "HPshortened": "HP",
"ATK": "Attack", "ATK": "Attack",
"ATKshortened": "Atk", "ATKshortened": "Atk",
"DEF": "Defense", "DEF": "Defense",
@ -13,8 +13,7 @@
"SPD": "Speed", "SPD": "Speed",
"SPDshortened": "Spd", "SPDshortened": "Spd",
"ACC": "Accuracy", "ACC": "Accuracy",
"EVA": "Evasiveness", "EVA": "Evasiveness"
"HPStat": "HP"
}, },
"Type": { "Type": {
"UNKNOWN": "Unknown", "UNKNOWN": "Unknown",
@ -38,4 +37,4 @@
"FAIRY": "Fairy", "FAIRY": "Fairy",
"STELLAR": "Stellar" "STELLAR": "Stellar"
} }
} }

View File

@ -91,7 +91,7 @@
"name": "Campeón Liga Master", "name": "Campeón Liga Master",
"name_female": "Campeona Liga Master" "name_female": "Campeona Liga Master"
}, },
"TRANSFER_MAX_BATTLE_STAT": { "TRANSFER_MAX_STAT_STAGE": {
"name": "Trabajo en Equipo", "name": "Trabajo en Equipo",
"description": "Haz relevo a otro miembro del equipo con al menos una estadística al máximo." "description": "Haz relevo a otro miembro del equipo con al menos una estadística al máximo."
}, },
@ -175,4 +175,4 @@
"name": "Espejo ojepsE", "name": "Espejo ojepsE",
"description": "Completa el reto de Combate Inverso.\n.osrevnI etabmoC ed oter le atelpmoC" "description": "Completa el reto de Combate Inverso.\n.osrevnI etabmoC ed oter le atelpmoC"
} }
} }

View File

@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": { "DoubleBattleChanceBoosterModifierType": {
"description": "Duplica la posibilidad de que un encuentro sea una combate doble durante {{battleCount}} combates." "description": "Duplica la posibilidad de que un encuentro sea una combate doble durante {{battleCount}} combates."
}, },
"TempBattleStatBoosterModifierType": { "TempStatStageBoosterModifierType": {
"description": "Aumenta la est. {{tempBattleStatName}} de todos los miembros del equipo en 1 nivel durante 5 combates." "description": "Aumenta la est. {{stat}} de todos los miembros del equipo en 1 nivel durante 5 combates."
}, },
"AttackTypeBoosterModifierType": { "AttackTypeBoosterModifierType": {
"description": "Aumenta la potencia de los movimientos de tipo {{moveType}} de un Pokémon en un 20%." "description": "Aumenta la potencia de los movimientos de tipo {{moveType}} de un Pokémon en un 20%."
@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": { "AllPokemonLevelIncrementModifierType": {
"description": "Aumenta el nivel de todos los miembros del equipo en {{levels}}." "description": "Aumenta el nivel de todos los miembros del equipo en {{levels}}."
}, },
"PokemonBaseStatBoosterModifierType": { "BaseStatBoosterModifierType": {
"description": "Aumenta la est. {{statName}} base del portador en un 10%.\nCuanto mayores sean tus IVs, mayor será el límite de acumulación." "description": "Aumenta la est. {{stat}} base del portador en un 10%.\nCuanto mayores sean tus IVs, mayor será el límite de acumulación."
}, },
"AllPokemonFullHpRestoreModifierType": { "AllPokemonFullHpRestoreModifierType": {
"description": "Restaura el 100% de los PS de todos los Pokémon." "description": "Restaura el 100% de los PS de todos los Pokémon."
@ -248,6 +248,12 @@
"name": "Periscopio", "name": "Periscopio",
"description": "Aumenta la probabilidad de asestar un golpe crítico." "description": "Aumenta la probabilidad de asestar un golpe crítico."
}, },
"DIRE_HIT": {
"name": "Crítico X",
"extra": {
"raises": "Critical Hit Ratio"
}
},
"LEEK": { "LEEK": {
"name": "Puerro", "name": "Puerro",
"description": "Puerro muy largo y duro que aumenta la probabilidad de asestar un golpe crítico. Debe llevarlo Farfetch'd." "description": "Puerro muy largo y duro que aumenta la probabilidad de asestar un golpe crítico. Debe llevarlo Farfetch'd."
@ -411,25 +417,13 @@
"description": "Polvo muy fino, pero a la vez poderoso, que aumenta la Velocidad. Debe llevarlo Ditto." "description": "Polvo muy fino, pero a la vez poderoso, que aumenta la Velocidad. Debe llevarlo Ditto."
} }
}, },
"TempBattleStatBoosterItem": { "TempStatStageBoosterItem": {
"x_attack": "Ataque X", "x_attack": "Ataque X",
"x_defense": "Defensa X", "x_defense": "Defensa X",
"x_sp_atk": "Ataq. Esp. X", "x_sp_atk": "Ataq. Esp. X",
"x_sp_def": "Def. Esp. X", "x_sp_def": "Def. Esp. X",
"x_speed": "Velocidad X", "x_speed": "Velocidad X",
"x_accuracy": "Precisión X", "x_accuracy": "Precisión X"
"dire_hit": "Crítico X"
},
"TempBattleStatBoosterStatName": {
"ATK": "Ataque",
"DEF": "Defensa",
"SPATK": "Ataq. Esp.",
"SPDEF": "Def. Esp.",
"SPD": "Velocidad",
"ACC": "Precisión",
"CRIT": "Tasa de crítico",
"EVA": "Evasión",
"DEFAULT": "???"
}, },
"AttackTypeBoosterItem": { "AttackTypeBoosterItem": {
"silk_scarf": "Pañuelo seda", "silk_scarf": "Pañuelo seda",

View File

@ -1,4 +1,8 @@
{ {
"switchedTwoStatChanges": "{{pokemonName}} ha intercambiado los cambios en {{firstStat}} y {{secondStat}} con los del objetivo!",
"switchedStat": "{{pokemonName}} cambia su {{stat}} por la de su objetivo!",
"sharedGuard": "{{pokemonName}} suma su capacidad defensiva a la del objetivo y la reparte equitativamente!",
"sharedPower": "{{pokemonName}} suma su capacidad ofensiva a la del objetivo y la reparte equitativamente!",
"isChargingPower": "¡{{pokemonName}} está acumulando energía!", "isChargingPower": "¡{{pokemonName}} está acumulando energía!",
"burnedItselfOut": "¡El fuego interior de {{pokemonName}} se ha extinguido!", "burnedItselfOut": "¡El fuego interior de {{pokemonName}} se ha extinguido!",
"startedHeatingUpBeak": "¡{{pokemonName}} empieza\na calentar su pico!", "startedHeatingUpBeak": "¡{{pokemonName}} empieza\na calentar su pico!",
@ -9,4 +13,4 @@
"statEliminated": "¡Los cambios en estadísticas fueron eliminados!", "statEliminated": "¡Los cambios en estadísticas fueron eliminados!",
"revivalBlessing": "¡{{pokemonName}} ha revivido!", "revivalBlessing": "¡{{pokemonName}} ha revivido!",
"safeguard": "¡{{targetName}} está protegido por Velo Sagrado!" "safeguard": "¡{{targetName}} está protegido por Velo Sagrado!"
} }

View File

@ -92,7 +92,7 @@
"name": "Master Maitre de la Ligue", "name": "Master Maitre de la Ligue",
"name_female": "Master Maitresse de la Ligue" "name_female": "Master Maitresse de la Ligue"
}, },
"TRANSFER_MAX_BATTLE_STAT": { "TRANSFER_MAX_STAT_STAGE": {
"name": "Travail déquipe", "name": "Travail déquipe",
"description": "Utiliser Relais avec au moins une statistique montée à fond." "description": "Utiliser Relais avec au moins une statistique montée à fond."
}, },

View File

@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": { "DoubleBattleChanceBoosterModifierType": {
"description": "Double les chances de tomber sur un combat double pendant {{battleCount}} combats." "description": "Double les chances de tomber sur un combat double pendant {{battleCount}} combats."
}, },
"TempBattleStatBoosterModifierType": { "TempStatStageBoosterModifierType": {
"description": "Augmente dun cran {{tempBattleStatName}} pour toute léquipe pendant 5 combats." "description": "Augmente dun cran {{stat}} pour toute léquipe pendant 5 combats."
}, },
"AttackTypeBoosterModifierType": { "AttackTypeBoosterModifierType": {
"description": "Augmente de 20% la puissance des capacités de type {{moveType}} dun Pokémon." "description": "Augmente de 20% la puissance des capacités de type {{moveType}} dun Pokémon."
@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": { "AllPokemonLevelIncrementModifierType": {
"description": "Fait monter toute léquipe de {{levels}} niveau·x." "description": "Fait monter toute léquipe de {{levels}} niveau·x."
}, },
"PokemonBaseStatBoosterModifierType": { "BaseStatBoosterModifierType": {
"description": "Augmente de 10% {{statName}} de base de son porteur. Plus les IV sont hauts, plus il peut en porter." "description": "Augmente de 10% {{stat}} de base de son porteur. Plus les IV sont hauts, plus il peut en porter."
}, },
"AllPokemonFullHpRestoreModifierType": { "AllPokemonFullHpRestoreModifierType": {
"description": "Restaure tous les PV de toute léquipe." "description": "Restaure tous les PV de toute léquipe."
@ -183,6 +183,7 @@
"SOOTHE_BELL": { "name": "Grelot Zen" }, "SOOTHE_BELL": { "name": "Grelot Zen" },
"SCOPE_LENS": { "name": "Lentilscope", "description": "Une lentille qui augmente dun cran le taux de critiques du porteur." }, "SCOPE_LENS": { "name": "Lentilscope", "description": "Une lentille qui augmente dun cran le taux de critiques du porteur." },
"DIRE_HIT": { "name": "Muscle +", "extra": { "raises": "Taux de critique" } },
"LEEK": { "name": "Poireau", "description": "À faire tenir à Canarticho ou Palarticho. Un poireau très long et solide qui augmente de 2 crans le taux de critiques." }, "LEEK": { "name": "Poireau", "description": "À faire tenir à Canarticho ou Palarticho. Un poireau très long et solide qui augmente de 2 crans le taux de critiques." },
"EVIOLITE": { "name": "Évoluroc", "description": "Augmente de 50% la Défense et Déf. Spé. si le porteur peut évoluer, 25% aux fusions dont une moitié le peut encore." }, "EVIOLITE": { "name": "Évoluroc", "description": "Augmente de 50% la Défense et Déf. Spé. si le porteur peut évoluer, 25% aux fusions dont une moitié le peut encore." },
@ -250,28 +251,14 @@
"METAL_POWDER": { "name": "Poudre Métal", "description": "À faire tenir à Métamorph. Cette poudre étrange, très fine mais résistante, double sa Défense." }, "METAL_POWDER": { "name": "Poudre Métal", "description": "À faire tenir à Métamorph. Cette poudre étrange, très fine mais résistante, double sa Défense." },
"QUICK_POWDER": { "name": "Poudre Vite", "description": "À faire tenir à Métamorph. Cette poudre étrange, très fine mais résistante, double sa Vitesse." } "QUICK_POWDER": { "name": "Poudre Vite", "description": "À faire tenir à Métamorph. Cette poudre étrange, très fine mais résistante, double sa Vitesse." }
}, },
"TempBattleStatBoosterItem": { "TempStatStageBoosterItem": {
"x_attack": "Attaque +", "x_attack": "Attaque +",
"x_defense": "Défense +", "x_defense": "Défense +",
"x_sp_atk": "Atq. Spé. +", "x_sp_atk": "Atq. Spé. +",
"x_sp_def": "Déf. Spé. +", "x_sp_def": "Déf. Spé. +",
"x_speed": "Vitesse +", "x_speed": "Vitesse +",
"x_accuracy": "Précision +", "x_accuracy": "Précision +"
"dire_hit": "Muscle +"
}, },
"TempBattleStatBoosterStatName": {
"ATK": "lAttaque",
"DEF": "la Défense",
"SPATK": "lAtq. Spé.",
"SPDEF": "la Déf. Spé.",
"SPD": "la Vitesse",
"ACC": "la précision",
"CRIT": "le taux de critique",
"EVA": "lesquive",
"DEFAULT": "???"
},
"AttackTypeBoosterItem": { "AttackTypeBoosterItem": {
"silk_scarf": "Mouchoir Soie", "silk_scarf": "Mouchoir Soie",
"black_belt": "Ceinture Noire", "black_belt": "Ceinture Noire",

View File

@ -3,7 +3,7 @@
"turnHealApply": "Les PV de {{pokemonNameWithAffix}}\nsont un peu restaurés par les {{typeName}} !", "turnHealApply": "Les PV de {{pokemonNameWithAffix}}\nsont un peu restaurés par les {{typeName}} !",
"hitHealApply": "Les PV de {{pokemonNameWithAffix}}\nsont un peu restaurés par le {{typeName}} !", "hitHealApply": "Les PV de {{pokemonNameWithAffix}}\nsont un peu restaurés par le {{typeName}} !",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}} a repris connaissance\navec sa {{typeName}} et est prêt à se battre de nouveau !", "pokemonInstantReviveApply": "{{pokemonNameWithAffix}} a repris connaissance\navec sa {{typeName}} et est prêt à se battre de nouveau !",
"pokemonResetNegativeStatStageApply": "Les stats baissées de {{pokemonNameWithAffix}}\nsont restaurées par l{{typeName}} !", "resetNegativeStatStageApply": "Les stats baissées de {{pokemonNameWithAffix}}\nsont restaurées par l{{typeName}} !",
"moneyInterestApply": "La {{typeName}} vous rapporte\n{{moneyAmount}}  dintérêts !", "moneyInterestApply": "La {{typeName}} vous rapporte\n{{moneyAmount}}  dintérêts !",
"turnHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} est absorbé·e\npar le {{typeName}} de {{pokemonName}} !", "turnHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} est absorbé·e\npar le {{typeName}} de {{pokemonName}} !",
"contactHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} est volé·e\npar l{{typeName}} de {{pokemonName}} !", "contactHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} est volé·e\npar l{{typeName}} de {{pokemonName}} !",

View File

@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}} sacrifie des PV\net augmente la puissance ses capacités !", "cutHpPowerUpMove": "{{pokemonName}} sacrifie des PV\net augmente la puissance ses capacités !",
"absorbedElectricity": "{{pokemonName}} absorbe de lélectricité !", "absorbedElectricity": "{{pokemonName}} absorbe de lélectricité !",
"switchedStatChanges": "{{pokemonName}} permute\nles changements de stats avec ceux de sa cible !", "switchedStatChanges": "{{pokemonName}} permute\nles changements de stats avec ceux de sa cible !",
"switchedTwoStatChanges": "{{pokemonName}} permute les changements de {{firstStat} et de {{secondStat}} avec ceux de sa cible !",
"switchedStat": "{{pokemonName}} et sa cible échangent leur {{stat}} !",
"sharedGuard": "{{pokemonName}} additionne sa garde à celle de sa cible et redistribue le tout équitablement !",
"sharedPower": "{{pokemonName}} additionne sa force à celle de sa cible et redistribue le tout équitablement !",
"goingAllOutForAttack": "{{pokemonName}} a pris\ncette capacité au sérieux !", "goingAllOutForAttack": "{{pokemonName}} a pris\ncette capacité au sérieux !",
"regainedHealth": "{{pokemonName}}\nrécupère des PV !", "regainedHealth": "{{pokemonName}}\nrécupère des PV !",
"keptGoingAndCrashed": "{{pokemonName}}\nsécrase au sol !", "keptGoingAndCrashed": "{{pokemonName}}\nsécrase au sol !",

View File

@ -80,7 +80,7 @@
"100_RIBBONS": { "100_RIBBONS": {
"name": "Campione Lega Assoluta" "name": "Campione Lega Assoluta"
}, },
"TRANSFER_MAX_BATTLE_STAT": { "TRANSFER_MAX_STAT_STAGE": {
"name": "Lavoro di Squadra", "name": "Lavoro di Squadra",
"description": "Trasferisci almeno sei bonus statistiche tramite staffetta" "description": "Trasferisci almeno sei bonus statistiche tramite staffetta"
}, },
@ -261,4 +261,4 @@
"name": "Buona la prima!", "name": "Buona la prima!",
"description": "Completa la modalità sfida 'Un nuovo inizio'." "description": "Completa la modalità sfida 'Un nuovo inizio'."
} }
} }

View File

@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": { "DoubleBattleChanceBoosterModifierType": {
"description": "Raddoppia la possibilità di imbattersi in doppie battaglie per {{battleCount}} battaglie." "description": "Raddoppia la possibilità di imbattersi in doppie battaglie per {{battleCount}} battaglie."
}, },
"TempBattleStatBoosterModifierType": { "TempStatStageBoosterModifierType": {
"description": "Aumenta {{tempBattleStatName}} di un livello a tutti i Pokémon nel gruppo per 5 battaglie." "description": "Aumenta la statistica {{stat}} di un livello\na tutti i Pokémon nel gruppo per 5 battaglie."
}, },
"AttackTypeBoosterModifierType": { "AttackTypeBoosterModifierType": {
"description": "Aumenta la potenza delle mosse di tipo {{moveType}} del 20% per un Pokémon." "description": "Aumenta la potenza delle mosse di tipo {{moveType}} del 20% per un Pokémon."
@ -59,10 +59,10 @@
"description": "Aumenta il livello di un Pokémon di {{levels}}." "description": "Aumenta il livello di un Pokémon di {{levels}}."
}, },
"AllPokemonLevelIncrementModifierType": { "AllPokemonLevelIncrementModifierType": {
"description": "Aumenta i livell di tutti i Pokémon della squadra di {{levels}}." "description": "Aumenta il livello di tutti i Pokémon della squadra di {{levels}}."
}, },
"PokemonBaseStatBoosterModifierType": { "BaseStatBoosterModifierType": {
"description": "Aumenta {{statName}} di base del possessore del 10%." "description": "Aumenta l'/la {{stat}} di base del possessore del 10%."
}, },
"AllPokemonFullHpRestoreModifierType": { "AllPokemonFullHpRestoreModifierType": {
"description": "Restituisce il 100% dei PS a tutti i Pokémon." "description": "Restituisce il 100% dei PS a tutti i Pokémon."
@ -248,6 +248,12 @@
"name": "Mirino", "name": "Mirino",
"description": "Lente che aumenta la probabilità di sferrare brutti colpi." "description": "Lente che aumenta la probabilità di sferrare brutti colpi."
}, },
"DIRE_HIT": {
"name": "Supercolpo",
"extra": {
"raises": "Tasso di brutti colpi"
}
},
"LEEK": { "LEEK": {
"name": "Porro", "name": "Porro",
"description": "Strumento da dare a Farfetch'd. Lungo gambo di porro che aumenta la probabilità di sferrare brutti colpi." "description": "Strumento da dare a Farfetch'd. Lungo gambo di porro che aumenta la probabilità di sferrare brutti colpi."
@ -411,25 +417,13 @@
"description": "Strumento da dare a Ditto. Questa strana polvere, fine e al contempo dura, aumenta la Velocità." "description": "Strumento da dare a Ditto. Questa strana polvere, fine e al contempo dura, aumenta la Velocità."
} }
}, },
"TempBattleStatBoosterItem": { "TempStatStageBoosterItem": {
"x_attack": "Attacco X", "x_attack": "Attacco X",
"x_defense": "Difesa X", "x_defense": "Difesa X",
"x_sp_atk": "Att. Speciale X", "x_sp_atk": "Att. Speciale X",
"x_sp_def": "Dif. Speciale X", "x_sp_def": "Dif. Speciale X",
"x_speed": "Velocità X", "x_speed": "Velocità X",
"x_accuracy": "Precisione X", "x_accuracy": "Precisione X"
"dire_hit": "Supercolpo"
},
"TempBattleStatBoosterStatName": {
"ATK": "Attacco",
"DEF": "Difesa",
"SPATK": "Att. Speciale",
"SPDEF": "Dif. Speciale",
"SPD": "Velocità",
"ACC": "Precisione",
"CRIT": "Tasso di brutti colpi",
"EVA": "Elusione",
"DEFAULT": "???"
}, },
"AttackTypeBoosterItem": { "AttackTypeBoosterItem": {
"silk_scarf": "Sciarpa seta", "silk_scarf": "Sciarpa seta",
@ -606,4 +600,4 @@
"FAIRY_MEMORY": "ROM Folletto", "FAIRY_MEMORY": "ROM Folletto",
"NORMAL_MEMORY": "ROM Normale" "NORMAL_MEMORY": "ROM Normale"
} }
} }

View File

@ -3,7 +3,7 @@
"turnHealApply": "{{pokemonNameWithAffix}} recupera alcuni PS con\nil/la suo/a {{typeName}}!", "turnHealApply": "{{pokemonNameWithAffix}} recupera alcuni PS con\nil/la suo/a {{typeName}}!",
"hitHealApply": "{{pokemonNameWithAffix}} recupera alcuni PS con\nil/la suo/a {{typeName}}!", "hitHealApply": "{{pokemonNameWithAffix}} recupera alcuni PS con\nil/la suo/a {{typeName}}!",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}} torna in forze\ngrazie al/alla suo/a {{typeName}}!", "pokemonInstantReviveApply": "{{pokemonNameWithAffix}} torna in forze\ngrazie al/alla suo/a {{typeName}}!",
"pokemonResetNegativeStatStageApply": "La riduzione alle statistiche di {{pokemonNameWithAffix}}\nviene annullata grazie al/alla suo/a {{typeName}}!", "resetNegativeStatStageApply": "La riduzione alle statistiche di {{pokemonNameWithAffix}}\nviene annullata grazie al/alla suo/a {{typeName}}!",
"moneyInterestApply": "Ricevi un interesse pari a {{moneyAmount}}₽\ngrazie al/allo/a {{typeName}}!", "moneyInterestApply": "Ricevi un interesse pari a {{moneyAmount}}₽\ngrazie al/allo/a {{typeName}}!",
"turnHeldItemTransferApply": "Il/l'/lo/la {{itemName}} di {{pokemonNameWithAffix}} è stato assorbito\ndal {{typeName}} di {{pokemonName}}!", "turnHeldItemTransferApply": "Il/l'/lo/la {{itemName}} di {{pokemonNameWithAffix}} è stato assorbito\ndal {{typeName}} di {{pokemonName}}!",
"contactHeldItemTransferApply": "Il/l'/lo/la {{itemName}} di {{pokemonNameWithAffix}} è stato rubato\nda {{pokemonName}} con {{typeName}}!", "contactHeldItemTransferApply": "Il/l'/lo/la {{itemName}} di {{pokemonNameWithAffix}} è stato rubato\nda {{pokemonName}} con {{typeName}}!",

View File

@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}} riduce i suoi PS per potenziare la sua mossa!", "cutHpPowerUpMove": "{{pokemonName}} riduce i suoi PS per potenziare la sua mossa!",
"absorbedElectricity": "{{pokemonName}} assorbe elettricità!", "absorbedElectricity": "{{pokemonName}} assorbe elettricità!",
"switchedStatChanges": "{{pokemonName}} scambia con il bersaglio le modifiche alle statistiche!", "switchedStatChanges": "{{pokemonName}} scambia con il bersaglio le modifiche alle statistiche!",
"switchedTwoStatChanges": "{{pokemonName}} scambia con il bersaglio le modifiche a {{firstStat}} e {{secondStat}}!",
"switchedStat": "{{pokemonName}} scambia la sua {{stat}} con quella del bersaglio!",
"sharedGuard": "{{pokemonName}} somma le sue capacità difensive con quelle del bersaglio e le ripartisce equamente!",
"sharedPower": "{{pokemonName}} somma le sue capacità offensive con quelle del bersaglio e le ripartisce equamente!",
"goingAllOutForAttack": "{{pokemonName}} fa sul serio!", "goingAllOutForAttack": "{{pokemonName}} fa sul serio!",
"regainedHealth": "{{pokemonName}} s'è\nripreso!", "regainedHealth": "{{pokemonName}} s'è\nripreso!",
"keptGoingAndCrashed": "{{pokemonName}} si sbilancia e\nsi schianta!", "keptGoingAndCrashed": "{{pokemonName}} si sbilancia e\nsi schianta!",

View File

@ -81,7 +81,7 @@
"100_RIBBONS": { "100_RIBBONS": {
"name": "マスターリーグチャンピオン" "name": "マスターリーグチャンピオン"
}, },
"TRANSFER_MAX_BATTLE_STAT": { "TRANSFER_MAX_STAT_STAGE": {
"name": "同力", "name": "同力",
"description": "少なくとも 一つの 能力を 最大まで あげて\n他の 手持ちポケモンに バトンタッチする" "description": "少なくとも 一つの 能力を 最大まで あげて\n他の 手持ちポケモンに バトンタッチする"
}, },

View File

@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": { "DoubleBattleChanceBoosterModifierType": {
"description": "バトル{{battleCount}}かいのあいだ ダブルバトルになるかくりつを2ばいにする" "description": "バトル{{battleCount}}かいのあいだ ダブルバトルになるかくりつを2ばいにする"
}, },
"TempBattleStatBoosterModifierType": { "TempStatStageBoosterModifierType": {
"description": "すべてのパーティメンバーの {{tempBattleStatName}}を5かいのバトルのあいだ 1だんかいあげる" "description": "すべてのパーティメンバーの {{stat}}を5かいのバトルのあいだ 1だんかいあげる"
}, },
"AttackTypeBoosterModifierType": { "AttackTypeBoosterModifierType": {
"description": "ポケモンの {{moveType}}タイプのわざのいりょくを20パーセントあげる" "description": "ポケモンの {{moveType}}タイプのわざのいりょくを20パーセントあげる"
@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": { "AllPokemonLevelIncrementModifierType": {
"description": "すべてのパーティメンバーのレベルを1あげる" "description": "すべてのパーティメンバーのレベルを1あげる"
}, },
"PokemonBaseStatBoosterModifierType": { "BaseStatBoosterModifierType": {
"description": "ポケモンの{{statName}}のきほんステータスを10パーセントあげる。こたいちがたかいほどスタックのげんかいもたかくなる。" "description": "ポケモンの{{stat}}のきほんステータスを10パーセントあげる。こたいちがたかいほどスタックのげんかいもたかくなる。"
}, },
"AllPokemonFullHpRestoreModifierType": { "AllPokemonFullHpRestoreModifierType": {
"description": "すべてのポケモンのHPを100パーセントかいふくする" "description": "すべてのポケモンのHPを100パーセントかいふくする"
@ -248,6 +248,12 @@
"name": "ピントレンズ", "name": "ピントレンズ",
"description": "弱点が 見える レンズ。持たせた ポケモンの技が 急所に 当たりやすくなる。" "description": "弱点が 見える レンズ。持たせた ポケモンの技が 急所に 当たりやすくなる。"
}, },
"DIRE_HIT": {
"name": "クリティカット",
"extra": {
"raises": "きゅうしょりつ"
}
},
"LEEK": { "LEEK": {
"name": "ながねぎ", "name": "ながねぎ",
"description": "とても長くて 硬いクキ。カモネギに 持たせると 技が 急所に 当たりやすくなる。" "description": "とても長くて 硬いクキ。カモネギに 持たせると 技が 急所に 当たりやすくなる。"
@ -411,25 +417,13 @@
"description": "メタモンに 持たせると 素早さが あがる 不思議 粉。とても こまかくて 硬い。" "description": "メタモンに 持たせると 素早さが あがる 不思議 粉。とても こまかくて 硬い。"
} }
}, },
"TempBattleStatBoosterItem": { "TempStatStageBoosterItem": {
"x_attack": "プラスパワー", "x_attack": "プラスパワー",
"x_defense": "ディフェンダー", "x_defense": "ディフェンダー",
"x_sp_atk": "スペシャルアップ", "x_sp_atk": "スペシャルアップ",
"x_sp_def": "スペシャルガード", "x_sp_def": "スペシャルガード",
"x_speed": "スピーダー", "x_speed": "スピーダー",
"x_accuracy": "ヨクアタール", "x_accuracy": "ヨクアタール"
"dire_hit": "クリティカット"
},
"TempBattleStatBoosterStatName": {
"ATK": "こうげき",
"DEF": "ぼうぎょ",
"SPATK": "とくこう",
"SPDEF": "とくぼう",
"SPD": "すばやさ",
"ACC": "めいちゅう",
"CRIT": "きゅうしょりつ",
"EVA": "かいひ",
"DEFAULT": "???"
}, },
"AttackTypeBoosterItem": { "AttackTypeBoosterItem": {
"silk_scarf": "シルクのスカーフ", "silk_scarf": "シルクのスカーフ",
@ -569,4 +563,4 @@
"DOUSE_DRIVE": "アクアカセット", "DOUSE_DRIVE": "アクアカセット",
"ULTRANECROZIUM_Z": "ウルトラネクロZ" "ULTRANECROZIUM_Z": "ウルトラネクロZ"
} }
} }

View File

@ -3,7 +3,7 @@
"turnHealApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 少し 回復!", "turnHealApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 少し 回復!",
"hitHealApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 少し 回復!", "hitHealApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 少し 回復!",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 復活した!", "pokemonInstantReviveApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 復活した!",
"pokemonResetNegativeStatStageApply": "{{pokemonNameWithAffix}}は {{typeName}}で\n下がった能力が 元に戻った", "resetNegativeStatStageApply": "{{pokemonNameWithAffix}}は {{typeName}}で\n下がった能力が 元に戻った",
"moneyInterestApply": "{{typeName}}から {{moneyAmount}}円 取得した!", "moneyInterestApply": "{{typeName}}から {{moneyAmount}}円 取得した!",
"turnHeldItemTransferApply": "{{pokemonName}}の {{typeName}}が\n{{pokemonNameWithAffix}}の {{itemName}}を 吸い取った!", "turnHeldItemTransferApply": "{{pokemonName}}の {{typeName}}が\n{{pokemonNameWithAffix}}の {{itemName}}を 吸い取った!",
"contactHeldItemTransferApply": "{{pokemonName}}の {{typeName}}が\n{{pokemonNameWithAffix}}の {{itemName}}を うばい取った!", "contactHeldItemTransferApply": "{{pokemonName}}の {{typeName}}が\n{{pokemonNameWithAffix}}の {{itemName}}を うばい取った!",

View File

@ -2,7 +2,9 @@
"hitWithRecoil": "{{pokemonName}}は\nはんどうによる ダメージを うけた", "hitWithRecoil": "{{pokemonName}}は\nはんどうによる ダメージを うけた",
"cutHpPowerUpMove": "{{pokemonName}}は\nたいりょくを けずって パワーぜんかい", "cutHpPowerUpMove": "{{pokemonName}}は\nたいりょくを けずって パワーぜんかい",
"absorbedElectricity": "{{pokemonName}}は\n でんきを きゅうしゅうした", "absorbedElectricity": "{{pokemonName}}は\n でんきを きゅうしゅうした",
"switchedStatChanges": "{{pokemonName}}は あいてと じぶんのn\nのうりょくへんかを いれかえた", "switchedStatChanges": "{{pokemonName}}は あいてと じぶんの\nのうりょくへんかを いれかえた",
"sharedGuard": "{{pokemonName}}は\nおたがいのガードを シェアした",
"sharedPower": "{{pokemonName}}は\nおたがいのパワーを シェアした",
"goingAllOutForAttack": "{{pokemonName}}は\nほんきを だした", "goingAllOutForAttack": "{{pokemonName}}は\nほんきを だした",
"regainedHealth": "{{pokemonName}}は\nたいりょくを かいふくした", "regainedHealth": "{{pokemonName}}は\nたいりょくを かいふくした",
"keptGoingAndCrashed": "いきおいあまって {{pokemonName}}は\nじめんに ぶつかった", "keptGoingAndCrashed": "いきおいあまって {{pokemonName}}は\nじめんに ぶつかった",
@ -59,4 +61,4 @@
"suppressAbilities": "{{pokemonName}}の とくせいが きかなくなった!", "suppressAbilities": "{{pokemonName}}の とくせいが きかなくなった!",
"revivalBlessing": "{{pokemonName}}は\n復活して 戦えるようになった", "revivalBlessing": "{{pokemonName}}は\n復活して 戦えるようになった",
"swapArenaTags": "{{pokemonName}}は\nおたがいの ばのこうかを いれかえた" "swapArenaTags": "{{pokemonName}}は\nおたがいの ばのこうかを いれかえた"
} }

View File

@ -80,7 +80,7 @@
"100_RIBBONS": { "100_RIBBONS": {
"name": "마스터 리그 챔피언" "name": "마스터 리그 챔피언"
}, },
"TRANSFER_MAX_BATTLE_STAT": { "TRANSFER_MAX_STAT_STAGE": {
"name": "팀워크", "name": "팀워크",
"description": "한 개 이상의 능력치가 최대 랭크일 때 배턴터치 사용" "description": "한 개 이상의 능력치가 최대 랭크일 때 배턴터치 사용"
}, },

View File

@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": { "DoubleBattleChanceBoosterModifierType": {
"description": "{{battleCount}}번의 배틀 동안 더블 배틀이 등장할 확률이 두 배가 된다." "description": "{{battleCount}}번의 배틀 동안 더블 배틀이 등장할 확률이 두 배가 된다."
}, },
"TempBattleStatBoosterModifierType": { "TempStatStageBoosterModifierType": {
"description": "자신의 모든 포켓몬이 5번의 배틀 동안 {{tempBattleStatName}}[[가]] 한 단계 증가한다." "description": "자신의 모든 포켓몬이 5번의 배틀 동안 {{stat}}[[가]] 한 단계 증가한다."
}, },
"AttackTypeBoosterModifierType": { "AttackTypeBoosterModifierType": {
"description": "지니게 하면 {{moveType}}타입 기술의 위력이 20% 상승한다." "description": "지니게 하면 {{moveType}}타입 기술의 위력이 20% 상승한다."
@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": { "AllPokemonLevelIncrementModifierType": {
"description": "자신의 모든 포켓몬의 레벨이 {{levels}}만큼 상승한다." "description": "자신의 모든 포켓몬의 레벨이 {{levels}}만큼 상승한다."
}, },
"PokemonBaseStatBoosterModifierType": { "BaseStatBoosterModifierType": {
"description": "지니게 하면 {{statName}} 종족값을 10% 올려준다. 개체값이 높을수록 더 많이 누적시킬 수 있다." "description": "지니게 하면 {{stat}} 종족값을 10% 올려준다. 개체값이 높을수록 더 많이 누적시킬 수 있다."
}, },
"AllPokemonFullHpRestoreModifierType": { "AllPokemonFullHpRestoreModifierType": {
"description": "자신의 포켓몬의 HP를 모두 회복한다." "description": "자신의 포켓몬의 HP를 모두 회복한다."
@ -248,6 +248,12 @@
"name": "초점렌즈", "name": "초점렌즈",
"description": "약점이 보이는 렌즈. 지니게 한 포켓몬의 기술이 급소에 맞기 쉬워진다." "description": "약점이 보이는 렌즈. 지니게 한 포켓몬의 기술이 급소에 맞기 쉬워진다."
}, },
"DIRE_HIT": {
"name": "크리티컬커터",
"extra": {
"raises": "급소율"
}
},
"LEEK": { "LEEK": {
"name": "대파", "name": "대파",
"description": "매우 길고 단단한 줄기. 파오리에게 지니게 하면 기술이 급소에 맞기 쉬워진다." "description": "매우 길고 단단한 줄기. 파오리에게 지니게 하면 기술이 급소에 맞기 쉬워진다."
@ -411,25 +417,13 @@
"description": "메타몽에게 지니게 하면 스피드가 올라가는 이상한 가루. 매우 잘고 단단하다." "description": "메타몽에게 지니게 하면 스피드가 올라가는 이상한 가루. 매우 잘고 단단하다."
} }
}, },
"TempBattleStatBoosterItem": { "TempStatStageBoosterItem": {
"x_attack": "플러스파워", "x_attack": "플러스파워",
"x_defense": "디펜드업", "x_defense": "디펜드업",
"x_sp_atk": "스페셜업", "x_sp_atk": "스페셜업",
"x_sp_def": "스페셜가드", "x_sp_def": "스페셜가드",
"x_speed": "스피드업", "x_speed": "스피드업",
"x_accuracy": "잘-맞히기", "x_accuracy": "잘-맞히기"
"dire_hit": "크리티컬커터"
},
"TempBattleStatBoosterStatName": {
"ATK": "공격",
"DEF": "방어",
"SPATK": "특수공격",
"SPDEF": "특수방어",
"SPD": "스피드",
"ACC": "명중률",
"CRIT": "급소율",
"EVA": "회피율",
"DEFAULT": "???"
}, },
"AttackTypeBoosterItem": { "AttackTypeBoosterItem": {
"silk_scarf": "실크스카프", "silk_scarf": "실크스카프",
@ -606,4 +600,4 @@
"FAIRY_MEMORY": "페어리메모리", "FAIRY_MEMORY": "페어리메모리",
"NORMAL_MEMORY": "일반메모리" "NORMAL_MEMORY": "일반메모리"
} }
} }

View File

@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}}[[는]]\n체력을 깎아서 자신의 기술을 강화했다!", "cutHpPowerUpMove": "{{pokemonName}}[[는]]\n체력을 깎아서 자신의 기술을 강화했다!",
"absorbedElectricity": "{{pokemonName}}는(은)\n전기를 흡수했다!", "absorbedElectricity": "{{pokemonName}}는(은)\n전기를 흡수했다!",
"switchedStatChanges": "{{pokemonName}}[[는]] 상대와 자신의\n능력 변화를 바꿨다!", "switchedStatChanges": "{{pokemonName}}[[는]] 상대와 자신의\n능력 변화를 바꿨다!",
"switchedTwoStatChanges": "{{pokemonName}} 상대와 자신의 {{firstStat}}과 {{secondStat}}의 능력 변화를 바꿨다!",
"switchedStat": "{{pokemonName}} 서로의 {{stat}}를 교체했다!",
"sharedGuard": "{{pokemonName}} 서로의 가드를 셰어했다!",
"sharedPower": "{{pokemonName}} 서로의 파워를 셰어했다!",
"goingAllOutForAttack": "{{pokemonName}}[[는]]\n전력을 다하기 시작했다!", "goingAllOutForAttack": "{{pokemonName}}[[는]]\n전력을 다하기 시작했다!",
"regainedHealth": "{{pokemonName}}[[는]]\n기력을 회복했다!", "regainedHealth": "{{pokemonName}}[[는]]\n기력을 회복했다!",
"keptGoingAndCrashed": "{{pokemonName}}[[는]]\n의욕이 넘쳐서 땅에 부딪쳤다!", "keptGoingAndCrashed": "{{pokemonName}}[[는]]\n의욕이 넘쳐서 땅에 부딪쳤다!",
@ -63,4 +67,4 @@
"swapArenaTags": "{{pokemonName}}[[는]]\n서로의 필드 효과를 교체했다!", "swapArenaTags": "{{pokemonName}}[[는]]\n서로의 필드 효과를 교체했다!",
"exposedMove": "{{pokemonName}}[[는]]\n{{targetPokemonName}}의 정체를 꿰뚫어 보았다!", "exposedMove": "{{pokemonName}}[[는]]\n{{targetPokemonName}}의 정체를 꿰뚫어 보았다!",
"safeguard": "{{targetName}}[[는]] 신비의 베일이 지켜 주고 있다!" "safeguard": "{{targetName}}[[는]] 신비의 베일이 지켜 주고 있다!"
} }

View File

@ -84,7 +84,7 @@
"100_RIBBONS": { "100_RIBBONS": {
"name": "Fita de Diamante" "name": "Fita de Diamante"
}, },
"TRANSFER_MAX_BATTLE_STAT": { "TRANSFER_MAX_STAT_STAGE": {
"name": "Trabalho em Equipe", "name": "Trabalho em Equipe",
"description": "Use Baton Pass com pelo menos um atributo aumentado ao máximo" "description": "Use Baton Pass com pelo menos um atributo aumentado ao máximo"
}, },
@ -269,4 +269,4 @@
"name": "A torre da derrotA", "name": "A torre da derrotA",
"description": "Complete o desafio da Batalha Inversa.\n.asrevnI ahlataB ad oifased o etelpmoC" "description": "Complete o desafio da Batalha Inversa.\n.asrevnI ahlataB ad oifased o etelpmoC"
} }
} }

View File

@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": { "DoubleBattleChanceBoosterModifierType": {
"description": "Dobra as chances de encontrar uma batalha em dupla por {{battleCount}} batalhas." "description": "Dobra as chances de encontrar uma batalha em dupla por {{battleCount}} batalhas."
}, },
"TempBattleStatBoosterModifierType": { "TempStatStageBoosterModifierType": {
"description": "Aumenta o atributo de {{tempBattleStatName}} para todos os membros da equipe por 5 batalhas." "description": "Aumenta o atributo de {{stat}} para todos os membros da equipe por 5 batalhas."
}, },
"AttackTypeBoosterModifierType": { "AttackTypeBoosterModifierType": {
"description": "Aumenta o poder dos ataques do tipo {{moveType}} de um Pokémon em 20%." "description": "Aumenta o poder dos ataques do tipo {{moveType}} de um Pokémon em 20%."
@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": { "AllPokemonLevelIncrementModifierType": {
"description": "Aumenta em {{levels}} o nível de todos os membros da equipe." "description": "Aumenta em {{levels}} o nível de todos os membros da equipe."
}, },
"PokemonBaseStatBoosterModifierType": { "BaseStatBoosterModifierType": {
"description": "Aumenta o atributo base de {{statName}} em 10%. Quanto maior os IVs, maior o limite de aumento." "description": "Aumenta o atributo base de {{stat}} em 10%. Quanto maior os IVs, maior o limite de aumento."
}, },
"AllPokemonFullHpRestoreModifierType": { "AllPokemonFullHpRestoreModifierType": {
"description": "Restaura totalmente os PS de todos os Pokémon." "description": "Restaura totalmente os PS de todos os Pokémon."
@ -248,6 +248,12 @@
"name": "Lentes de Mira", "name": "Lentes de Mira",
"description": "Estas lentes facilitam o foco em pontos fracos. Aumenta a chance de acerto crítico de quem a segurar." "description": "Estas lentes facilitam o foco em pontos fracos. Aumenta a chance de acerto crítico de quem a segurar."
}, },
"DIRE_HIT": {
"name": "Direto",
"extra": {
"raises": "Chance de Acerto Crítico"
}
},
"LEEK": { "LEEK": {
"name": "Alho-poró", "name": "Alho-poró",
"description": "Esse talo de alho-poró muito longo e rígido aumenta a taxa de acerto crítico dos movimentos do Farfetch'd." "description": "Esse talo de alho-poró muito longo e rígido aumenta a taxa de acerto crítico dos movimentos do Farfetch'd."
@ -411,25 +417,13 @@
"description": "Extremamente fino, porém duro, este pó estranho aumenta o atributo de Velocidade de Ditto." "description": "Extremamente fino, porém duro, este pó estranho aumenta o atributo de Velocidade de Ditto."
} }
}, },
"TempBattleStatBoosterItem": { "TempStatStageBoosterItem": {
"x_attack": "Ataque X", "x_attack": "Ataque X",
"x_defense": "Defesa X", "x_defense": "Defesa X",
"x_sp_atk": "Ataque Esp. X", "x_sp_atk": "Ataque Esp. X",
"x_sp_def": "Defesa Esp. X", "x_sp_def": "Defesa Esp. X",
"x_speed": "Velocidade X", "x_speed": "Velocidade X",
"x_accuracy": "Precisão X", "x_accuracy": "Precisão X"
"dire_hit": "Direto"
},
"TempBattleStatBoosterStatName": {
"ATK": "Ataque",
"DEF": "Defesa",
"SPATK": "Ataque Esp.",
"SPDEF": "Defesa Esp.",
"SPD": "Velocidade",
"ACC": "Precisão",
"CRIT": "Chance de Acerto Crítico",
"EVA": "Evasão",
"DEFAULT": "???"
}, },
"AttackTypeBoosterItem": { "AttackTypeBoosterItem": {
"silk_scarf": "Lenço de Seda", "silk_scarf": "Lenço de Seda",

View File

@ -3,7 +3,7 @@
"turnHealApply": "{{pokemonNameWithAffix}} restaurou um pouco de PS usando\nsuas {{typeName}}!", "turnHealApply": "{{pokemonNameWithAffix}} restaurou um pouco de PS usando\nsuas {{typeName}}!",
"hitHealApply": "{{pokemonNameWithAffix}} restaurou um pouco de PS usando\nsua {{typeName}}!", "hitHealApply": "{{pokemonNameWithAffix}} restaurou um pouco de PS usando\nsua {{typeName}}!",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}} foi reanimado\npor sua {{typeName}}!", "pokemonInstantReviveApply": "{{pokemonNameWithAffix}} foi reanimado\npor sua {{typeName}}!",
"pokemonResetNegativeStatStageApply": "Os atributos diminuídos de {{pokemonNameWithAffix}} foram\nrestaurados por seu(sua) {{typeName}}!", "resetNegativeStatStageApply": "Os atributos diminuídos de {{pokemonNameWithAffix}} foram\nrestaurados por seu(sua) {{typeName}}!",
"moneyInterestApply": "Você recebeu um juros de ₽{{moneyAmount}}\nde sua {{typeName}}!", "moneyInterestApply": "Você recebeu um juros de ₽{{moneyAmount}}\nde sua {{typeName}}!",
"turnHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} foi absorvido(a)\npelo {{typeName}} de {{pokemonName}}!", "turnHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} foi absorvido(a)\npelo {{typeName}} de {{pokemonName}}!",
"contactHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} foi pego(a)\npela {{typeName}} de {{pokemonName}}!", "contactHeldItemTransferApply": "{{itemName}} de {{pokemonNameWithAffix}} foi pego(a)\npela {{typeName}} de {{pokemonName}}!",

View File

@ -63,4 +63,4 @@
"swapArenaTags": "{{pokemonName}} trocou os efeitos de batalha que afetam cada lado do campo!", "swapArenaTags": "{{pokemonName}} trocou os efeitos de batalha que afetam cada lado do campo!",
"exposedMove": "{{pokemonName}} identificou\n{{targetPokemonName}}!", "exposedMove": "{{pokemonName}} identificou\n{{targetPokemonName}}!",
"safeguard": "{{targetName}} está protegido por Safeguard!" "safeguard": "{{targetName}} está protegido por Safeguard!"
} }

View File

@ -86,7 +86,7 @@
"name": "大师球联盟冠军" "name": "大师球联盟冠军"
}, },
"TRANSFER_MAX_BATTLE_STAT": { "TRANSFER_MAX_STAT_STAGE": {
"name": "团队协作", "name": "团队协作",
"description": "在一项属性强化至最大时用接力棒传递给其他宝可梦" "description": "在一项属性强化至最大时用接力棒传递给其他宝可梦"
}, },

View File

@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": { "DoubleBattleChanceBoosterModifierType": {
"description": "接下来的{{battleCount}}场战斗是双打的概率翻倍。" "description": "接下来的{{battleCount}}场战斗是双打的概率翻倍。"
}, },
"TempBattleStatBoosterModifierType": { "TempStatStageBoosterModifierType": {
"description": "为所有成员宝可梦提升一级{{tempBattleStatName}}持续5场战斗。" "description": "为所有成员宝可梦提升一级{{stat}}持续5场战斗。"
}, },
"AttackTypeBoosterModifierType": { "AttackTypeBoosterModifierType": {
"description": "一只宝可梦的{{moveType}}系招式威力提升20%。" "description": "一只宝可梦的{{moveType}}系招式威力提升20%。"
@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": { "AllPokemonLevelIncrementModifierType": {
"description": "使一只寶可夢的等級提升{{levels}}級。" "description": "使一只寶可夢的等級提升{{levels}}級。"
}, },
"PokemonBaseStatBoosterModifierType": { "BaseStatBoosterModifierType": {
"description": "增加10%持有者的{{statName}}\n个体值越高堆叠上限越高。" "description": "增加10%持有者的{{stat}}\n个体值越高堆叠上限越高。"
}, },
"AllPokemonFullHpRestoreModifierType": { "AllPokemonFullHpRestoreModifierType": {
"description": "所有宝可梦完全回复HP。" "description": "所有宝可梦完全回复HP。"
@ -248,6 +248,12 @@
"name": "焦点镜", "name": "焦点镜",
"description": "能看见弱点的镜片。携带它的宝可梦的招式\n会变得容易击中要害。" "description": "能看见弱点的镜片。携带它的宝可梦的招式\n会变得容易击中要害。"
}, },
"DIRE_HIT": {
"name": "要害攻击",
"extra": {
"raises": "会心"
}
},
"LEEK": { "LEEK": {
"name": "大葱", "name": "大葱",
"description": "非常长且坚硬的茎。让大葱鸭携带后,\n招式会变得容易击中要害。" "description": "非常长且坚硬的茎。让大葱鸭携带后,\n招式会变得容易击中要害。"
@ -411,25 +417,13 @@
"description": "让百变怪携带后,速度就会提高的神奇粉末。\n非常细腻坚硬。" "description": "让百变怪携带后,速度就会提高的神奇粉末。\n非常细腻坚硬。"
} }
}, },
"TempBattleStatBoosterItem": { "TempStatStageBoosterItem": {
"x_attack": "力量强化", "x_attack": "力量强化",
"x_defense": "防御强化", "x_defense": "防御强化",
"x_sp_atk": "特攻强化", "x_sp_atk": "特攻强化",
"x_sp_def": "特防强化", "x_sp_def": "特防强化",
"x_speed": "速度强化", "x_speed": "速度强化",
"x_accuracy": "命中强化", "x_accuracy": "命中强化"
"dire_hit": "要害攻击"
},
"TempBattleStatBoosterStatName": {
"ATK": "攻击",
"DEF": "防御",
"SPATK": "特攻",
"SPDEF": "特防",
"SPD": "速度",
"ACC": "命中",
"CRIT": "会心",
"EVA": "闪避",
"DEFAULT": "???"
}, },
"AttackTypeBoosterItem": { "AttackTypeBoosterItem": {
"silk_scarf": "丝绸围巾", "silk_scarf": "丝绸围巾",
@ -606,4 +600,4 @@
"FAIRY_MEMORY": "妖精存储碟", "FAIRY_MEMORY": "妖精存储碟",
"NORMAL_MEMORY": "一般存储碟" "NORMAL_MEMORY": "一般存储碟"
} }
} }

View File

@ -3,7 +3,7 @@
"turnHealApply": "{{pokemonNameWithAffix}}用{{typeName}}\n回复了体力", "turnHealApply": "{{pokemonNameWithAffix}}用{{typeName}}\n回复了体力",
"hitHealApply": "{{pokemonNameWithAffix}}用{{typeName}}\n回复了体力", "hitHealApply": "{{pokemonNameWithAffix}}用{{typeName}}\n回复了体力",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}}用{{typeName}}\n恢复了活力", "pokemonInstantReviveApply": "{{pokemonNameWithAffix}}用{{typeName}}\n恢复了活力",
"pokemonResetNegativeStatStageApply": "{{pokemonNameWithAffix}}降低的能力被{{typeName}}\n复原了", "resetNegativeStatStageApply": "{{pokemonNameWithAffix}}降低的能力被{{typeName}}\n复原了",
"moneyInterestApply": "用{{typeName}}\n获得了 ₽{{moneyAmount}} 利息!", "moneyInterestApply": "用{{typeName}}\n获得了 ₽{{moneyAmount}} 利息!",
"turnHeldItemTransferApply": "{{pokemonNameWithAffix}}的{{itemName}}被\n{{pokemonName}}的{{typeName}}吸收了!", "turnHeldItemTransferApply": "{{pokemonNameWithAffix}}的{{itemName}}被\n{{pokemonName}}的{{typeName}}吸收了!",
"contactHeldItemTransferApply": "{{pokemonNameWithAffix}}的{{itemName}}被\n{{pokemonName}}的{{typeName}}夺取了!", "contactHeldItemTransferApply": "{{pokemonNameWithAffix}}的{{itemName}}被\n{{pokemonName}}的{{typeName}}夺取了!",

View File

@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}}\n削减了体力并提升了招式威力", "cutHpPowerUpMove": "{{pokemonName}}\n削减了体力并提升了招式威力",
"absorbedElectricity": "{{pokemonName}}\n吸收了电力", "absorbedElectricity": "{{pokemonName}}\n吸收了电力",
"switchedStatChanges": "{{pokemonName}}和对手互换了\n自己的能力变化", "switchedStatChanges": "{{pokemonName}}和对手互换了\n自己的能力变化",
"switchedTwoStatChanges": "{{pokemonName}} 和对手互换了自己的{{firstStat}}和{{secondStat}}的能力变化!",
"switchedStat": "{{pokemonName}} 互换了各自的{{stat}}",
"sharedGuard": "{{pokemonName}} 平分了各自的防守!",
"sharedPower": "{{pokemonName}} 平分了各自的力量!",
"goingAllOutForAttack": "{{pokemonName}}拿出全力了!", "goingAllOutForAttack": "{{pokemonName}}拿出全力了!",
"regainedHealth": "{{pokemonName}}的\n体力回复了", "regainedHealth": "{{pokemonName}}的\n体力回复了",
"keptGoingAndCrashed": "{{pokemonName}}因势头过猛\n而撞到了地面", "keptGoingAndCrashed": "{{pokemonName}}因势头过猛\n而撞到了地面",

View File

@ -1,7 +1,7 @@
{ {
"Stat": { "Stat": {
"HP": "最大HP", "HP": "最大HP",
"HPshortened": "最大HP", "HPshortened": "HP",
"ATK": "攻击", "ATK": "攻击",
"ATKshortened": "攻击", "ATKshortened": "攻击",
"DEF": "防御", "DEF": "防御",
@ -37,4 +37,4 @@
"FAIRY": "妖精", "FAIRY": "妖精",
"STELLAR": "星晶" "STELLAR": "星晶"
} }
} }

View File

@ -80,7 +80,7 @@
"100_RIBBONS": { "100_RIBBONS": {
"name": "大師球聯盟冠軍" "name": "大師球聯盟冠軍"
}, },
"TRANSFER_MAX_BATTLE_STAT": { "TRANSFER_MAX_STAT_STAGE": {
"name": "團隊協作", "name": "團隊協作",
"description": "在一項屬性強化至最大時用接力棒傳遞給其他寶可夢" "description": "在一項屬性強化至最大時用接力棒傳遞給其他寶可夢"
}, },
@ -257,4 +257,4 @@
"name": "鏡子子鏡", "name": "鏡子子鏡",
"description": "完成逆轉之戰挑戰\n戰挑戰之轉逆成完" "description": "完成逆轉之戰挑戰\n戰挑戰之轉逆成完"
} }
} }

View File

@ -49,8 +49,8 @@
"DoubleBattleChanceBoosterModifierType": { "DoubleBattleChanceBoosterModifierType": {
"description": "接下來的{{battleCount}}場戰鬥是雙打的概率翻倍。" "description": "接下來的{{battleCount}}場戰鬥是雙打的概率翻倍。"
}, },
"TempBattleStatBoosterModifierType": { "TempStatStageBoosterModifierType": {
"description": "爲所有成員寶可夢提升一級{{tempBattleStatName}}持續5場戰鬥。" "description": "爲所有成員寶可夢提升一級{{stat}}持續5場戰鬥。"
}, },
"AttackTypeBoosterModifierType": { "AttackTypeBoosterModifierType": {
"description": "一隻寶可夢的{{moveType}}系招式威力提升20%。" "description": "一隻寶可夢的{{moveType}}系招式威力提升20%。"
@ -61,8 +61,8 @@
"AllPokemonLevelIncrementModifierType": { "AllPokemonLevelIncrementModifierType": {
"description": "Increases all party members' level by {{levels}}." "description": "Increases all party members' level by {{levels}}."
}, },
"PokemonBaseStatBoosterModifierType": { "BaseStatBoosterModifierType": {
"description": "增加持有者的{{statName}}10%,個體值越高堆疊\n上限越高。" "description": "增加持有者的{{stat}}10%,個體值越高堆疊\n上限越高。"
}, },
"AllPokemonFullHpRestoreModifierType": { "AllPokemonFullHpRestoreModifierType": {
"description": "所有寶可夢完全恢復HP。" "description": "所有寶可夢完全恢復HP。"
@ -244,6 +244,12 @@
"name": "焦點鏡", "name": "焦點鏡",
"description": "能看見弱點的鏡片。攜帶它的寶可夢的招式 會變得容易擊中要害。" "description": "能看見弱點的鏡片。攜帶它的寶可夢的招式 會變得容易擊中要害。"
}, },
"DIRE_HIT": {
"name": "要害攻擊",
"extra": {
"raises": "會心"
}
},
"LEEK": { "LEEK": {
"name": "大蔥", "name": "大蔥",
"description": "非常長且堅硬的莖。讓大蔥鴨攜帶後,招式會 變得容易擊中要害。" "description": "非常長且堅硬的莖。讓大蔥鴨攜帶後,招式會 變得容易擊中要害。"
@ -407,25 +413,13 @@
"description": "讓百變怪攜帶後,速度就會提高的神奇粉末。非常細緻堅硬。" "description": "讓百變怪攜帶後,速度就會提高的神奇粉末。非常細緻堅硬。"
} }
}, },
"TempBattleStatBoosterItem": { "TempStatStageBoosterItem": {
"x_attack": "力量強化", "x_attack": "力量強化",
"x_defense": "防禦強化", "x_defense": "防禦強化",
"x_sp_atk": "特攻強化", "x_sp_atk": "特攻強化",
"x_sp_def": "特防強化", "x_sp_def": "特防強化",
"x_speed": "速度強化", "x_speed": "速度強化",
"x_accuracy": "命中強化", "x_accuracy": "命中強化"
"dire_hit": "要害攻擊"
},
"TempBattleStatBoosterStatName": {
"ATK": "攻擊",
"DEF": "防禦",
"SPATK": "特攻",
"SPDEF": "特防",
"SPD": "速度",
"ACC": "命中",
"CRIT": "會心",
"EVA": "閃避",
"DEFAULT": "???"
}, },
"AttackTypeBoosterItem": { "AttackTypeBoosterItem": {
"silk_scarf": "絲綢圍巾", "silk_scarf": "絲綢圍巾",
@ -602,4 +596,4 @@
"FAIRY_MEMORY": "妖精記憶碟", "FAIRY_MEMORY": "妖精記憶碟",
"NORMAL_MEMORY": "一般記憶碟" "NORMAL_MEMORY": "一般記憶碟"
} }
} }

View File

@ -8,4 +8,4 @@
"contactHeldItemTransferApply": "{{pokemonNameWithAffix}}的{{itemName}}被\n{{pokemonName}}的{{typeName}}奪取了!", "contactHeldItemTransferApply": "{{pokemonNameWithAffix}}的{{itemName}}被\n{{pokemonName}}的{{typeName}}奪取了!",
"enemyTurnHealApply": "{{pokemonNameWithAffix}}\n回復了一些體力", "enemyTurnHealApply": "{{pokemonNameWithAffix}}\n回復了一些體力",
"bypassSpeedChanceApply": "{{pokemonName}}用了{{itemName}}後,行動變快了!" "bypassSpeedChanceApply": "{{pokemonName}}用了{{itemName}}後,行動變快了!"
} }

View File

@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}}\n削減體力並提升了招式威力", "cutHpPowerUpMove": "{{pokemonName}}\n削減體力並提升了招式威力",
"absorbedElectricity": "{{pokemonName}}\n吸收了电力", "absorbedElectricity": "{{pokemonName}}\n吸收了电力",
"switchedStatChanges": "{{pokemonName}}和對手互換了\n自身的能力變化", "switchedStatChanges": "{{pokemonName}}和對手互換了\n自身的能力變化",
"switchedTwoStatChanges": "{{pokemonName}} 和對手互換了自身的{{firstStat}}和{{secondStat}}的能力變化!",
"switchedStat": "{{pokemonName}} 互換了各自的{{stat}}",
"sharedGuard": "{{pokemonName}} 平分了各自的防守!",
"sharedPower": "{{pokemonName}} 平分了各自的力量!",
"goingAllOutForAttack": "{{pokemonName}}拿出全力了!", "goingAllOutForAttack": "{{pokemonName}}拿出全力了!",
"regainedHealth": "{{pokemonName}}的\n體力回復了", "regainedHealth": "{{pokemonName}}的\n體力回復了",
"keptGoingAndCrashed": "{{pokemonName}}因勢頭過猛\n而撞到了地面", "keptGoingAndCrashed": "{{pokemonName}}因勢頭過猛\n而撞到了地面",

View File

@ -3,12 +3,10 @@ import { AttackMove, allMoves, selfStatLowerMoves } from "../data/move";
import { MAX_PER_TYPE_POKEBALLS, PokeballType, getPokeballCatchMultiplier, getPokeballName } from "../data/pokeball"; import { MAX_PER_TYPE_POKEBALLS, PokeballType, getPokeballCatchMultiplier, getPokeballName } from "../data/pokeball";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "../field/pokemon"; import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "../field/pokemon";
import { EvolutionItem, pokemonEvolutions } from "../data/pokemon-evolutions"; import { EvolutionItem, pokemonEvolutions } from "../data/pokemon-evolutions";
import { Stat, getStatName } from "../data/pokemon-stat";
import { tmPoolTiers, tmSpecies } from "../data/tms"; import { tmPoolTiers, tmSpecies } from "../data/tms";
import { Type } from "../data/type"; import { Type } from "../data/type";
import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "../ui/party-ui-handler"; import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "../ui/party-ui-handler";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { TempBattleStat, getTempBattleStatBoosterItemName, getTempBattleStatName } from "../data/temp-battle-stat";
import { getBerryEffectDescription, getBerryName } from "../data/berry"; import { getBerryEffectDescription, getBerryName } from "../data/berry";
import { Unlockables } from "../system/unlockables"; import { Unlockables } from "../system/unlockables";
import { StatusEffect, getStatusEffectDescriptor } from "../data/status-effect"; import { StatusEffect, getStatusEffectDescriptor } from "../data/status-effect";
@ -28,6 +26,7 @@ import { BerryType } from "#enums/berry-type";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { getPokemonNameWithAffix } from "#app/messages.js"; import { getPokemonNameWithAffix } from "#app/messages.js";
import { PermanentStat, TEMP_BATTLE_STATS, TempBattleStat, Stat, getStatKey } from "#app/enums/stat";
const outputModifierData = false; const outputModifierData = false;
const useMaxWeightForOutput = false; const useMaxWeightForOutput = false;
@ -447,26 +446,28 @@ export class DoubleBattleChanceBoosterModifierType extends ModifierType {
} }
} }
export class TempBattleStatBoosterModifierType extends ModifierType implements GeneratedPersistentModifierType { export class TempStatStageBoosterModifierType extends ModifierType implements GeneratedPersistentModifierType {
public tempBattleStat: TempBattleStat; private stat: TempBattleStat;
private key: string;
constructor(tempBattleStat: TempBattleStat) { constructor(stat: TempBattleStat) {
super("", getTempBattleStatBoosterItemName(tempBattleStat).replace(/\./g, "").replace(/[ ]/g, "_").toLowerCase(), const key = TempStatStageBoosterModifierTypeGenerator.items[stat];
(_type, _args) => new Modifiers.TempBattleStatBoosterModifier(this, this.tempBattleStat)); super("", key, (_type, _args) => new Modifiers.TempStatStageBoosterModifier(this, this.stat));
this.tempBattleStat = tempBattleStat; this.stat = stat;
this.key = key;
} }
get name(): string { get name(): string {
return i18next.t(`modifierType:TempBattleStatBoosterItem.${getTempBattleStatBoosterItemName(this.tempBattleStat).replace(/\./g, "").replace(/[ ]/g, "_").toLowerCase()}`); return i18next.t(`modifierType:TempStatStageBoosterItem.${this.key}`);
} }
getDescription(scene: BattleScene): string { getDescription(_scene: BattleScene): string {
return i18next.t("modifierType:ModifierType.TempBattleStatBoosterModifierType.description", { tempBattleStatName: getTempBattleStatName(this.tempBattleStat) }); return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { stat: i18next.t(getStatKey(this.stat)) });
} }
getPregenArgs(): any[] { getPregenArgs(): any[] {
return [ this.tempBattleStat ]; return [ this.stat ];
} }
} }
@ -611,40 +612,24 @@ export class AllPokemonLevelIncrementModifierType extends ModifierType {
} }
} }
function getBaseStatBoosterItemName(stat: Stat) { export class BaseStatBoosterModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType {
switch (stat) { private stat: PermanentStat;
case Stat.HP: private key: string;
return "HP Up";
case Stat.ATK:
return "Protein";
case Stat.DEF:
return "Iron";
case Stat.SPATK:
return "Calcium";
case Stat.SPDEF:
return "Zinc";
case Stat.SPD:
return "Carbos";
}
}
export class PokemonBaseStatBoosterModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType { constructor(stat: PermanentStat) {
private localeName: string; const key = BaseStatBoosterModifierTypeGenerator.items[stat];
private stat: Stat; super("", key, (_type, args) => new Modifiers.BaseStatModifier(this, (args[0] as Pokemon).id, this.stat));
constructor(localeName: string, stat: Stat) {
super("", localeName.replace(/[ \-]/g, "_").toLowerCase(), (_type, args) => new Modifiers.PokemonBaseStatModifier(this, (args[0] as Pokemon).id, this.stat));
this.localeName = localeName;
this.stat = stat; this.stat = stat;
this.key = key;
} }
get name(): string { get name(): string {
return i18next.t(`modifierType:BaseStatBoosterItem.${this.localeName.replace(/[ \-]/g, "_").toLowerCase()}`); return i18next.t(`modifierType:BaseStatBoosterItem.${this.key}`);
} }
getDescription(scene: BattleScene): string { getDescription(_scene: BattleScene): string {
return i18next.t("modifierType:ModifierType.PokemonBaseStatBoosterModifierType.description", { statName: getStatName(this.stat) }); return i18next.t("modifierType:ModifierType.BaseStatBoosterModifierType.description", { stat: i18next.t(getStatKey(this.stat)) });
} }
getPregenArgs(): any[] { getPregenArgs(): any[] {
@ -922,6 +907,48 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator {
} }
} }
class BaseStatBoosterModifierTypeGenerator extends ModifierTypeGenerator {
public static readonly items: Record<PermanentStat, string> = {
[Stat.HP]: "hp_up",
[Stat.ATK]: "protein",
[Stat.DEF]: "iron",
[Stat.SPATK]: "calcium",
[Stat.SPDEF]: "zinc",
[Stat.SPD]: "carbos"
};
constructor() {
super((_party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs) {
return new BaseStatBoosterModifierType(pregenArgs[0]);
}
const randStat: PermanentStat = Utils.randSeedInt(Stat.SPD + 1);
return new BaseStatBoosterModifierType(randStat);
});
}
}
class TempStatStageBoosterModifierTypeGenerator extends ModifierTypeGenerator {
public static readonly items: Record<TempBattleStat, string> = {
[Stat.ATK]: "x_attack",
[Stat.DEF]: "x_defense",
[Stat.SPATK]: "x_sp_atk",
[Stat.SPDEF]: "x_sp_def",
[Stat.SPD]: "x_speed",
[Stat.ACC]: "x_accuracy"
};
constructor() {
super((_party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs && (pregenArgs.length === 1) && TEMP_BATTLE_STATS.includes(pregenArgs[0])) {
return new TempStatStageBoosterModifierType(pregenArgs[0]);
}
const randStat: TempBattleStat = Utils.randSeedInt(Stat.ACC, Stat.ATK);
return new TempStatStageBoosterModifierType(randStat);
});
}
}
/** /**
* Modifier type generator for {@linkcode SpeciesStatBoosterModifierType}, which * Modifier type generator for {@linkcode SpeciesStatBoosterModifierType}, which
* encapsulates the logic for weighting the most useful held item from * encapsulates the logic for weighting the most useful held item from
@ -930,7 +957,7 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator {
*/ */
class SpeciesStatBoosterModifierTypeGenerator extends ModifierTypeGenerator { class SpeciesStatBoosterModifierTypeGenerator extends ModifierTypeGenerator {
/** Object comprised of the currently available species-based stat boosting held items */ /** Object comprised of the currently available species-based stat boosting held items */
public static items = { public static readonly items = {
LIGHT_BALL: { stats: [Stat.ATK, Stat.SPATK], multiplier: 2, species: [Species.PIKACHU] }, LIGHT_BALL: { stats: [Stat.ATK, Stat.SPATK], multiplier: 2, species: [Species.PIKACHU] },
THICK_CLUB: { stats: [Stat.ATK], multiplier: 2, species: [Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK] }, THICK_CLUB: { stats: [Stat.ATK], multiplier: 2, species: [Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK] },
METAL_POWDER: { stats: [Stat.DEF], multiplier: 2, species: [Species.DITTO] }, METAL_POWDER: { stats: [Stat.DEF], multiplier: 2, species: [Species.DITTO] },
@ -1233,7 +1260,7 @@ export type GeneratorModifierOverride = {
type?: SpeciesStatBoosterItem; type?: SpeciesStatBoosterItem;
} }
| { | {
name: keyof Pick<typeof modifierTypes, "TEMP_STAT_BOOSTER">; name: keyof Pick<typeof modifierTypes, "TEMP_STAT_STAGE_BOOSTER">;
type?: TempBattleStat; type?: TempBattleStat;
} }
| { | {
@ -1306,7 +1333,7 @@ export const modifierTypes = {
SACRED_ASH: () => new AllPokemonFullReviveModifierType("modifierType:ModifierType.SACRED_ASH", "sacred_ash"), SACRED_ASH: () => new AllPokemonFullReviveModifierType("modifierType:ModifierType.SACRED_ASH", "sacred_ash"),
REVIVER_SEED: () => new PokemonHeldItemModifierType("modifierType:ModifierType.REVIVER_SEED", "reviver_seed", (type, args) => new Modifiers.PokemonInstantReviveModifier(type, (args[0] as Pokemon).id)), REVIVER_SEED: () => new PokemonHeldItemModifierType("modifierType:ModifierType.REVIVER_SEED", "reviver_seed", (type, args) => new Modifiers.PokemonInstantReviveModifier(type, (args[0] as Pokemon).id)),
WHITE_HERB: () => new PokemonHeldItemModifierType("modifierType:ModifierType.WHITE_HERB", "white_herb", (type, args) => new Modifiers.PokemonResetNegativeStatStageModifier(type, (args[0] as Pokemon).id)), WHITE_HERB: () => new PokemonHeldItemModifierType("modifierType:ModifierType.WHITE_HERB", "white_herb", (type, args) => new Modifiers.ResetNegativeStatStageModifier(type, (args[0] as Pokemon).id)),
ETHER: () => new PokemonPpRestoreModifierType("modifierType:ModifierType.ETHER", "ether", 10), ETHER: () => new PokemonPpRestoreModifierType("modifierType:ModifierType.ETHER", "ether", 10),
MAX_ETHER: () => new PokemonPpRestoreModifierType("modifierType:ModifierType.MAX_ETHER", "max_ether", -1), MAX_ETHER: () => new PokemonPpRestoreModifierType("modifierType:ModifierType.MAX_ETHER", "max_ether", -1),
@ -1327,23 +1354,15 @@ export const modifierTypes = {
SPECIES_STAT_BOOSTER: () => new SpeciesStatBoosterModifierTypeGenerator(), SPECIES_STAT_BOOSTER: () => new SpeciesStatBoosterModifierTypeGenerator(),
TEMP_STAT_BOOSTER: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { TEMP_STAT_STAGE_BOOSTER: () => new TempStatStageBoosterModifierTypeGenerator(),
if (pregenArgs && (pregenArgs.length === 1) && (pregenArgs[0] in TempBattleStat)) {
return new TempBattleStatBoosterModifierType(pregenArgs[0] as TempBattleStat);
}
const randTempBattleStat = Utils.randSeedInt(6) as TempBattleStat;
return new TempBattleStatBoosterModifierType(randTempBattleStat);
}),
DIRE_HIT: () => new TempBattleStatBoosterModifierType(TempBattleStat.CRIT),
BASE_STAT_BOOSTER: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { DIRE_HIT: () => new class extends ModifierType {
if (pregenArgs && (pregenArgs.length === 1) && (pregenArgs[0] in Stat)) { getDescription(_scene: BattleScene): string {
const stat = pregenArgs[0] as Stat; return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { stat: i18next.t("modifierType:ModifierType.DIRE_HIT.extra.raises") });
return new PokemonBaseStatBoosterModifierType(getBaseStatBoosterItemName(stat), stat);
} }
const randStat = Utils.randSeedInt(6) as Stat; }("modifierType:ModifierType.DIRE_HIT", "dire_hit", (type, _args) => new Modifiers.TempCritBoosterModifier(type)),
return new PokemonBaseStatBoosterModifierType(getBaseStatBoosterItemName(randStat), randStat);
}), BASE_STAT_BOOSTER: () => new BaseStatBoosterModifierTypeGenerator(),
ATTACK_TYPE_BOOSTER: () => new AttackTypeBoosterModifierTypeGenerator(), ATTACK_TYPE_BOOSTER: () => new AttackTypeBoosterModifierTypeGenerator(),
@ -1513,7 +1532,7 @@ const modifierPool: ModifierPool = {
return thresholdPartyMemberCount; return thresholdPartyMemberCount;
}, 3), }, 3),
new WeightedModifierType(modifierTypes.LURE, skipInLastClassicWaveOrDefault(2)), new WeightedModifierType(modifierTypes.LURE, skipInLastClassicWaveOrDefault(2)),
new WeightedModifierType(modifierTypes.TEMP_STAT_BOOSTER, 4), new WeightedModifierType(modifierTypes.TEMP_STAT_STAGE_BOOSTER, 4),
new WeightedModifierType(modifierTypes.BERRY, 2), new WeightedModifierType(modifierTypes.BERRY, 2),
new WeightedModifierType(modifierTypes.TM_COMMON, 2), new WeightedModifierType(modifierTypes.TM_COMMON, 2),
].map(m => { ].map(m => {
@ -1626,7 +1645,7 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.WHITE_HERB, (party: Pokemon[]) => { new WeightedModifierType(modifierTypes.WHITE_HERB, (party: Pokemon[]) => {
const checkedAbilities = [Abilities.WEAK_ARMOR, Abilities.CONTRARY, Abilities.MOODY, Abilities.ANGER_SHELL, Abilities.COMPETITIVE, Abilities.DEFIANT]; const checkedAbilities = [Abilities.WEAK_ARMOR, Abilities.CONTRARY, Abilities.MOODY, Abilities.ANGER_SHELL, Abilities.COMPETITIVE, Abilities.DEFIANT];
const weightMultiplier = party.filter( const weightMultiplier = party.filter(
p => !p.getHeldItems().some(i => i instanceof Modifiers.PokemonResetNegativeStatStageModifier && i.stackCount >= i.getMaxHeldItemCount(p)) && p => !p.getHeldItems().some(i => i instanceof Modifiers.ResetNegativeStatStageModifier && i.stackCount >= i.getMaxHeldItemCount(p)) &&
(checkedAbilities.some(a => p.hasAbility(a, false, true)) || p.getMoveset(true).some(m => m && selfStatLowerMoves.includes(m.moveId)))).length; (checkedAbilities.some(a => p.hasAbility(a, false, true)) || p.getMoveset(true).some(m => m && selfStatLowerMoves.includes(m.moveId)))).length;
// If a party member has one of the above moves or abilities and doesn't have max herbs, the herb will appear more frequently // If a party member has one of the above moves or abilities and doesn't have max herbs, the herb will appear more frequently
return 0 * (weightMultiplier ? 2 : 1) + (weightMultiplier ? weightMultiplier * 0 : 0); return 0 * (weightMultiplier ? 2 : 1) + (weightMultiplier ? weightMultiplier * 0 : 0);

View File

@ -3,14 +3,12 @@ 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 { addTextObject, TextStyle } from "../ui/text"; import { addTextObject, TextStyle } from "../ui/text";
import { Type } from "../data/type"; import { Type } from "../data/type";
import { EvolutionPhase } from "../phases/evolution-phase"; import { EvolutionPhase } from "../phases/evolution-phase";
import { FusionSpeciesFormEvolution, pokemonEvolutions, pokemonPrevolutions } from "../data/pokemon-evolutions"; import { FusionSpeciesFormEvolution, pokemonEvolutions, pokemonPrevolutions } from "../data/pokemon-evolutions";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { TempBattleStat } from "../data/temp-battle-stat";
import { getBerryEffectFunc, getBerryPredicate } from "../data/berry"; import { getBerryEffectFunc, getBerryPredicate } from "../data/berry";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
@ -23,6 +21,7 @@ import Overrides from "#app/overrides";
import { ModifierType, modifierTypes } from "./modifier-type"; import { ModifierType, modifierTypes } from "./modifier-type";
import { Command } from "#app/ui/command-ui-handler"; import { Command } from "#app/ui/command-ui-handler";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { Stat, type PermanentStat, type TempBattleStat, BATTLE_STATS, TEMP_BATTLE_STATS } from "#app/enums/stat";
import i18next from "i18next"; import i18next from "i18next";
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
@ -362,41 +361,160 @@ export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier
} }
} }
export class TempBattleStatBoosterModifier extends LapsingPersistentModifier { /**
private tempBattleStat: TempBattleStat; * Modifier used for party-wide items, specifically the X items, that
* temporarily increases the stat stage multiplier of the corresponding
* {@linkcode TempBattleStat}.
* @extends LapsingPersistentModifier
* @see {@linkcode apply}
*/
export class TempStatStageBoosterModifier extends LapsingPersistentModifier {
private stat: TempBattleStat;
private multiplierBoost: number;
constructor(type: ModifierTypes.TempBattleStatBoosterModifierType, tempBattleStat: TempBattleStat, battlesLeft?: integer, stackCount?: integer) { constructor(type: ModifierType, stat: TempBattleStat, battlesLeft?: number, stackCount?: number) {
super(type, battlesLeft || 5, stackCount); super(type, battlesLeft ?? 5, stackCount);
this.tempBattleStat = tempBattleStat; this.stat = stat;
// Note that, because we want X Accuracy to maintain its original behavior,
// it will increment as it did previously, directly to the stat stage.
this.multiplierBoost = stat !== Stat.ACC ? 0.3 : 1;
} }
match(modifier: Modifier): boolean { match(modifier: Modifier): boolean {
if (modifier instanceof TempBattleStatBoosterModifier) { if (modifier instanceof TempStatStageBoosterModifier) {
return (modifier as TempBattleStatBoosterModifier).tempBattleStat === this.tempBattleStat const modifierInstance = modifier as TempStatStageBoosterModifier;
&& (modifier as TempBattleStatBoosterModifier).battlesLeft === this.battlesLeft; return (modifierInstance.stat === this.stat);
} }
return false; return false;
} }
clone(): TempBattleStatBoosterModifier { clone() {
return new TempBattleStatBoosterModifier(this.type as ModifierTypes.TempBattleStatBoosterModifierType, this.tempBattleStat, this.battlesLeft, this.stackCount); return new TempStatStageBoosterModifier(this.type, this.stat, this.battlesLeft, this.stackCount);
} }
getArgs(): any[] { getArgs(): any[] {
return [ this.tempBattleStat, this.battlesLeft ]; return [ this.stat, this.battlesLeft ];
} }
apply(args: any[]): boolean { /**
const tempBattleStat = args[0] as TempBattleStat; * Checks if {@linkcode args} contains the necessary elements and if the
* incoming stat is matches {@linkcode stat}.
* @param args [0] {@linkcode TempBattleStat} being checked at the time
* [1] {@linkcode Utils.NumberHolder} N/A
* @returns true if the modifier can be applied, false otherwise
*/
shouldApply(args: any[]): boolean {
return args && (args.length === 2) && TEMP_BATTLE_STATS.includes(args[0]) && (args[0] === this.stat) && (args[1] instanceof Utils.NumberHolder);
}
if (tempBattleStat === this.tempBattleStat) { /**
const statLevel = args[1] as Utils.IntegerHolder; * Increases the incoming stat stage matching {@linkcode stat} by {@linkcode multiplierBoost}.
statLevel.value = Math.min(statLevel.value + 1, 6); * @param args [0] {@linkcode TempBattleStat} N/A
return true; * [1] {@linkcode Utils.NumberHolder} that holds the resulting value of the stat stage multiplier
*/
apply(args: any[]): boolean {
(args[1] as Utils.NumberHolder).value += this.multiplierBoost;
return true;
}
/**
* Goes through existing modifiers for any that match the selected modifier,
* which will then either add it to the existing modifiers if none were found
* or, if one was found, it will refresh {@linkcode battlesLeft}.
* @param modifiers {@linkcode PersistentModifier} array of the player's modifiers
* @param _virtual N/A
* @param _scene N/A
* @returns true if the modifier was successfully added or applied, false otherwise
*/
add(modifiers: PersistentModifier[], _virtual: boolean, _scene: BattleScene): boolean {
for (const modifier of modifiers) {
if (this.match(modifier)) {
const modifierInstance = modifier as TempStatStageBoosterModifier;
if (modifierInstance.getBattlesLeft() < 5) {
modifierInstance.battlesLeft = 5;
return true;
}
// should never get here
return false;
}
} }
return false; modifiers.push(this);
return true;
}
getMaxStackCount(_scene: BattleScene, _forThreshold?: boolean): number {
return 1;
}
}
/**
* Modifier used for party-wide items, namely Dire Hit, that
* temporarily increments the critical-hit stage
* @extends LapsingPersistentModifier
* @see {@linkcode apply}
*/
export class TempCritBoosterModifier extends LapsingPersistentModifier {
constructor(type: ModifierType, battlesLeft?: integer, stackCount?: number) {
super(type, battlesLeft || 5, stackCount);
}
clone() {
return new TempCritBoosterModifier(this.type, this.stackCount);
}
match(modifier: Modifier): boolean {
return (modifier instanceof TempCritBoosterModifier);
}
/**
* Checks if {@linkcode args} contains the necessary elements.
* @param args [1] {@linkcode Utils.NumberHolder} N/A
* @returns true if the critical-hit stage boost applies successfully
*/
shouldApply(args: any[]): boolean {
return args && (args.length === 1) && (args[0] instanceof Utils.NumberHolder);
}
/**
* Increases the current critical-hit stage value by 1.
* @param args [0] {@linkcode Utils.IntegerHolder} that holds the resulting critical-hit level
* @returns true if the critical-hit stage boost applies successfully
*/
apply(args: any[]): boolean {
(args[0] as Utils.NumberHolder).value++;
return true;
}
/**
* Goes through existing modifiers for any that match the selected modifier,
* which will then either add it to the existing modifiers if none were found
* or, if one was found, it will refresh {@linkcode battlesLeft}.
* @param modifiers {@linkcode PersistentModifier} array of the player's modifiers
* @param _virtual N/A
* @param _scene N/A
* @returns true if the modifier was successfully added or applied, false otherwise
*/
add(modifiers: PersistentModifier[], _virtual: boolean, _scene: BattleScene): boolean {
for (const modifier of modifiers) {
if (this.match(modifier)) {
const modifierInstance = modifier as TempCritBoosterModifier;
if (modifierInstance.getBattlesLeft() < 5) {
modifierInstance.battlesLeft = 5;
return true;
}
// should never get here
return false;
}
}
modifiers.push(this);
return true;
}
getMaxStackCount(_scene: BattleScene, _forThreshold?: boolean): number {
return 1;
} }
} }
@ -663,24 +781,30 @@ export class TerastallizeModifier extends LapsingPokemonHeldItemModifier {
} }
} }
export class PokemonBaseStatModifier extends PokemonHeldItemModifier { /**
protected stat: Stat; * Modifier used for held items, specifically vitamins like Carbos, Hp Up, etc., that
* increase the value of a given {@linkcode PermanentStat}.
* @extends LapsingPersistentModifier
* @see {@linkcode apply}
*/
export class BaseStatModifier extends PokemonHeldItemModifier {
protected stat: PermanentStat;
readonly isTransferrable: boolean = false; readonly isTransferrable: boolean = false;
constructor(type: ModifierTypes.PokemonBaseStatBoosterModifierType, pokemonId: integer, stat: Stat, stackCount?: integer) { constructor(type: ModifierType, pokemonId: integer, stat: PermanentStat, stackCount?: integer) {
super(type, pokemonId, stackCount); super(type, pokemonId, stackCount);
this.stat = stat; this.stat = stat;
} }
matchType(modifier: Modifier): boolean { matchType(modifier: Modifier): boolean {
if (modifier instanceof PokemonBaseStatModifier) { if (modifier instanceof BaseStatModifier) {
return (modifier as PokemonBaseStatModifier).stat === this.stat; return (modifier as BaseStatModifier).stat === this.stat;
} }
return false; return false;
} }
clone(): PersistentModifier { clone(): PersistentModifier {
return new PokemonBaseStatModifier(this.type as ModifierTypes.PokemonBaseStatBoosterModifierType, this.pokemonId, this.stat, this.stackCount); return new BaseStatModifier(this.type, this.pokemonId, this.stat, this.stackCount);
} }
getArgs(): any[] { getArgs(): any[] {
@ -688,12 +812,12 @@ export class PokemonBaseStatModifier extends PokemonHeldItemModifier {
} }
shouldApply(args: any[]): boolean { shouldApply(args: any[]): boolean {
return super.shouldApply(args) && args.length === 2 && args[1] instanceof Array; return super.shouldApply(args) && args.length === 2 && Array.isArray(args[1]);
} }
apply(args: any[]): boolean { apply(args: any[]): boolean {
args[1][this.stat] = Math.min(Math.floor(args[1][this.stat] * (1 + this.getStackCount() * 0.1)), 999999); const baseStats = args[1] as number[];
baseStats[this.stat] = Math.floor(baseStats[this.stat] * (1 + this.getStackCount() * 0.1));
return true; return true;
} }
@ -1398,42 +1522,48 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier {
} }
/** /**
* Modifier used for White Herb, which resets negative {@linkcode Stat} changes * Modifier used for held items, namely White Herb, that restore adverse stat
* stages in battle.
* @extends PokemonHeldItemModifier * @extends PokemonHeldItemModifier
* @see {@linkcode apply} * @see {@linkcode apply}
*/ */
export class PokemonResetNegativeStatStageModifier extends PokemonHeldItemModifier { export class ResetNegativeStatStageModifier extends PokemonHeldItemModifier {
constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) { constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) {
super(type, pokemonId, stackCount); super(type, pokemonId, stackCount);
} }
matchType(modifier: Modifier) { matchType(modifier: Modifier) {
return modifier instanceof PokemonResetNegativeStatStageModifier; return modifier instanceof ResetNegativeStatStageModifier;
} }
clone() { clone() {
return new PokemonResetNegativeStatStageModifier(this.type, this.pokemonId, this.stackCount); return new ResetNegativeStatStageModifier(this.type, this.pokemonId, this.stackCount);
} }
/** /**
* Restores any negative stat stages of the mon to 0 * Goes through the holder's stat stages and, if any are negative, resets that
* @param args args[0] is the {@linkcode Pokemon} whose stat stages are being checked * stat stage back to 0.
* @returns true if any stat changes were applied (item was used), false otherwise * @param args [0] {@linkcode Pokemon} that holds the held item
* @returns true if any stat stages were reset, false otherwise
*/ */
apply(args: any[]): boolean { apply(args: any[]): boolean {
const pokemon = args[0] as Pokemon; const pokemon = args[0] as Pokemon;
const loweredStats = pokemon.summonData.battleStats.filter(s => s < 0); let statRestored = false;
if (loweredStats.length) {
for (let s = 0; s < pokemon.summonData.battleStats.length; s++) { for (const s of BATTLE_STATS) {
pokemon.summonData.battleStats[s] = Math.max(0, pokemon.summonData.battleStats[s]); if (pokemon.getStatStage(s) < 0) {
pokemon.setStatStage(s, 0);
statRestored = true;
} }
pokemon.scene.queueMessage(i18next.t("modifier:pokemonResetNegativeStatStageApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }));
return true;
} }
return false;
if (statRestored) {
pokemon.scene.queueMessage(i18next.t("modifier:resetNegativeStatStageApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }));
}
return statRestored;
} }
getMaxHeldItemCount(pokemon: Pokemon): integer { getMaxHeldItemCount(_pokemon: Pokemon): integer {
return 2; return 2;
} }
} }
@ -2745,7 +2875,7 @@ export class EnemyFusionChanceModifier extends EnemyPersistentModifier {
* - The player * - The player
* - The enemy * - The enemy
* @param scene current {@linkcode BattleScene} * @param scene current {@linkcode BattleScene}
* @param isPlayer {@linkcode boolean} for whether the the player (`true`) or enemy (`false`) is being overridden * @param isPlayer {@linkcode boolean} for whether the player (`true`) or enemy (`false`) is being overridden
*/ */
export function overrideModifiers(scene: BattleScene, isPlayer: boolean = true): void { export function overrideModifiers(scene: BattleScene, isPlayer: boolean = true): void {
const modifiersOverride: ModifierTypes.ModifierOverride[] = isPlayer ? Overrides.STARTING_MODIFIER_OVERRIDE : Overrides.OPP_MODIFIER_OVERRIDE; const modifiersOverride: ModifierTypes.ModifierOverride[] = isPlayer ? Overrides.STARTING_MODIFIER_OVERRIDE : Overrides.OPP_MODIFIER_OVERRIDE;
@ -2760,13 +2890,22 @@ export function overrideModifiers(scene: BattleScene, isPlayer: boolean = true):
modifiersOverride.forEach(item => { modifiersOverride.forEach(item => {
const modifierFunc = modifierTypes[item.name]; const modifierFunc = modifierTypes[item.name];
const modifier = modifierFunc().withIdFromFunc(modifierFunc).newModifier() as PersistentModifier; let modifierType: ModifierType | null = modifierFunc();
modifier.stackCount = item.count || 1;
if (isPlayer) { if (modifierType instanceof ModifierTypes.ModifierTypeGenerator) {
scene.addModifier(modifier, true, false, false, true); const pregenArgs = ("type" in item) && (item.type !== null) ? [item.type] : undefined;
} else { modifierType = modifierType.generateType([], pregenArgs);
scene.addEnemyModifier(modifier, true, true); }
const modifier = modifierType && modifierType.withIdFromFunc(modifierFunc).newModifier() as PersistentModifier;
if (modifier) {
modifier.stackCount = item.count || 1;
if (isPlayer) {
scene.addModifier(modifier, true, false, false, true);
} else {
scene.addEnemyModifier(modifier, true, true);
}
} }
}); });
} }

View File

@ -1,14 +1,14 @@
import BattleScene from "#app/battle-scene.js"; import BattleScene from "#app/battle-scene";
import { BattlerIndex, BattleType } from "#app/battle.js"; import { BattlerIndex, BattleType } from "#app/battle";
import { applyPostFaintAbAttrs, PostFaintAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr } from "#app/data/ability.js"; import { applyPostFaintAbAttrs, PostFaintAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr } from "#app/data/ability";
import { BattlerTagLapseType } from "#app/data/battler-tags.js"; import { BattlerTagLapseType } from "#app/data/battler-tags";
import { battleSpecDialogue } from "#app/data/dialogue.js"; import { battleSpecDialogue } from "#app/data/dialogue";
import { allMoves, PostVictoryStatChangeAttr } from "#app/data/move.js"; import { allMoves, PostVictoryStatStageChangeAttr } from "#app/data/move";
import { BattleSpec } from "#app/enums/battle-spec.js"; import { BattleSpec } from "#app/enums/battle-spec";
import { StatusEffect } from "#app/enums/status-effect.js"; import { StatusEffect } from "#app/enums/status-effect";
import { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon.js"; import { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages.js"; import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonInstantReviveModifier } from "#app/modifier/modifier.js"; import { PokemonInstantReviveModifier } from "#app/modifier/modifier";
import i18next from "i18next"; import i18next from "i18next";
import { DamagePhase } from "./damage-phase"; import { DamagePhase } from "./damage-phase";
import { PokemonPhase } from "./pokemon-phase"; import { PokemonPhase } from "./pokemon-phase";
@ -72,7 +72,7 @@ export class FaintPhase extends PokemonPhase {
if (defeatSource?.isOnField()) { if (defeatSource?.isOnField()) {
applyPostVictoryAbAttrs(PostVictoryAbAttr, defeatSource); applyPostVictoryAbAttrs(PostVictoryAbAttr, defeatSource);
const pvmove = allMoves[pokemon.turnData.attacksReceived[0].move]; const pvmove = allMoves[pokemon.turnData.attacksReceived[0].move];
const pvattrs = pvmove.getAttrs(PostVictoryStatChangeAttr); const pvattrs = pvmove.getAttrs(PostVictoryStatStageChangeAttr);
if (pvattrs.length) { if (pvattrs.length) {
for (const pvattr of pvattrs) { for (const pvattr of pvattrs) {
pvattr.applyPostVictory(defeatSource, defeatSource, pvmove); pvattr.applyPostVictory(defeatSource, defeatSource, pvmove);

View File

@ -1,5 +1,5 @@
import Pokemon from "#app/field/pokemon.js";
import { BattlePhase } from "./battle-phase"; import { BattlePhase } from "./battle-phase";
import Pokemon from "#app/field/pokemon";
type PokemonFunc = (pokemon: Pokemon) => void; type PokemonFunc = (pokemon: Pokemon) => void;

View File

@ -1,248 +0,0 @@
import { BattlerIndex } from "#app/battle";
import BattleScene from "#app/battle-scene";
import { applyAbAttrs, applyPostStatChangeAbAttrs, applyPreStatChangeAbAttrs, PostStatChangeAbAttr, ProtectStatAbAttr, StatChangeCopyAbAttr, StatChangeMultiplierAbAttr } from "#app/data/ability";
import { ArenaTagSide, MistTag } from "#app/data/arena-tag";
import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "#app/data/battle-stat";
import Pokemon from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonResetNegativeStatStageModifier } from "#app/modifier/modifier";
import { handleTutorial, Tutorial } from "#app/tutorial";
import * as Utils from "#app/utils";
import i18next from "i18next";
import { PokemonPhase } from "./pokemon-phase";
export type StatChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void;
export class StatChangePhase extends PokemonPhase {
private stats: BattleStat[];
private selfTarget: boolean;
private levels: integer;
private showMessage: boolean;
private ignoreAbilities: boolean;
private canBeCopied: boolean;
private onChange: StatChangeCallback | null;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], levels: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatChangeCallback | null = null) {
super(scene, battlerIndex);
this.selfTarget = selfTarget;
this.stats = stats;
this.levels = levels;
this.showMessage = showMessage;
this.ignoreAbilities = ignoreAbilities;
this.canBeCopied = canBeCopied;
this.onChange = onChange;
}
start() {
const pokemon = this.getPokemon();
let random = false;
if (this.stats.length === 1 && this.stats[0] === BattleStat.RAND) {
this.stats[0] = this.getRandomStat();
random = true;
}
this.aggregateStatChanges(random);
if (!pokemon.isActive(true)) {
return this.end();
}
const filteredStats = this.stats.map(s => s !== BattleStat.RAND ? s : this.getRandomStat()).filter(stat => {
const cancelled = new Utils.BooleanHolder(false);
if (!this.selfTarget && this.levels < 0) {
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled);
}
if (!cancelled.value && !this.selfTarget && this.levels < 0) {
applyPreStatChangeAbAttrs(ProtectStatAbAttr, this.getPokemon(), stat, cancelled);
}
return !cancelled.value;
});
const levels = new Utils.IntegerHolder(this.levels);
if (!this.ignoreAbilities) {
applyAbAttrs(StatChangeMultiplierAbAttr, pokemon, null, false, levels);
}
const battleStats = this.getPokemon().summonData.battleStats;
const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats[stat] + levels.value, 6) : Math.max(battleStats[stat] + levels.value, -6)) - battleStats[stat]);
this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels);
const end = () => {
if (this.showMessage) {
const messages = this.getStatChangeMessages(filteredStats, levels.value, relLevels);
for (const message of messages) {
this.scene.queueMessage(message);
}
}
for (const stat of filteredStats) {
if (levels.value > 0 && pokemon.summonData.battleStats[stat] < 6) {
if (!pokemon.turnData) {
// Temporary fix for missing turn data struct on turn 1
pokemon.resetTurnData();
}
pokemon.turnData.battleStatsIncreased = true;
} else if (levels.value < 0 && pokemon.summonData.battleStats[stat] > -6) {
if (!pokemon.turnData) {
// Temporary fix for missing turn data struct on turn 1
pokemon.resetTurnData();
}
pokemon.turnData.battleStatsDecreased = true;
}
pokemon.summonData.battleStats[stat] = Math.max(Math.min(pokemon.summonData.battleStats[stat] + levels.value, 6), -6);
}
if (levels.value > 0 && this.canBeCopied) {
for (const opponent of pokemon.getOpponents()) {
applyAbAttrs(StatChangeCopyAbAttr, opponent, null, false, this.stats, levels.value);
}
}
applyPostStatChangeAbAttrs(PostStatChangeAbAttr, pokemon, filteredStats, this.levels, this.selfTarget);
// Look for any other stat change phases; if this is the last one, do White Herb check
const existingPhase = this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex);
if (!(existingPhase instanceof StatChangePhase)) {
// Apply White Herb if needed
const whiteHerb = this.scene.applyModifier(PokemonResetNegativeStatStageModifier, this.player, pokemon) as PokemonResetNegativeStatStageModifier;
// If the White Herb was applied, consume it
if (whiteHerb) {
--whiteHerb.stackCount;
if (whiteHerb.stackCount <= 0) {
this.scene.removeModifier(whiteHerb);
}
this.scene.updateModifiers(this.player);
}
}
pokemon.updateInfo();
handleTutorial(this.scene, Tutorial.Stat_Change).then(() => super.end());
};
if (relLevels.filter(l => l).length && this.scene.moveAnimations) {
pokemon.enableMask();
const pokemonMaskSprite = pokemon.maskSprite;
const tileX = (this.player ? 106 : 236) * pokemon.getSpriteScale() * this.scene.field.scale;
const tileY = ((this.player ? 148 : 84) + (levels.value >= 1 ? 160 : 0)) * pokemon.getSpriteScale() * this.scene.field.scale;
const tileWidth = 156 * this.scene.field.scale * pokemon.getSpriteScale();
const tileHeight = 316 * this.scene.field.scale * pokemon.getSpriteScale();
// On increase, show the red sprite located at ATK
// On decrease, show the blue sprite located at SPD
const spriteColor = levels.value >= 1 ? BattleStat[BattleStat.ATK].toLowerCase() : BattleStat[BattleStat.SPD].toLowerCase();
const statSprite = this.scene.add.tileSprite(tileX, tileY, tileWidth, tileHeight, "battle_stats", spriteColor);
statSprite.setPipeline(this.scene.fieldSpritePipeline);
statSprite.setAlpha(0);
statSprite.setScale(6);
statSprite.setOrigin(0.5, 1);
this.scene.playSound(`se/stat_${levels.value >= 1 ? "up" : "down"}`);
statSprite.setMask(new Phaser.Display.Masks.BitmapMask(this.scene, pokemonMaskSprite ?? undefined));
this.scene.tweens.add({
targets: statSprite,
duration: 250,
alpha: 0.8375,
onComplete: () => {
this.scene.tweens.add({
targets: statSprite,
delay: 1000,
duration: 250,
alpha: 0
});
}
});
this.scene.tweens.add({
targets: statSprite,
duration: 1500,
y: `${levels.value >= 1 ? "-" : "+"}=${160 * 6}`
});
this.scene.time.delayedCall(1750, () => {
pokemon.disableMask();
end();
});
} else {
end();
}
}
getRandomStat(): BattleStat {
const allStats = Utils.getEnumValues(BattleStat);
return this.getPokemon() ? allStats[this.getPokemon()!.randSeedInt(BattleStat.SPD + 1)] : BattleStat.ATK; // TODO: return default ATK on random? idk...
}
aggregateStatChanges(random: boolean = false): void {
const isAccEva = [BattleStat.ACC, BattleStat.EVA].some(s => this.stats.includes(s));
let existingPhase: StatChangePhase;
if (this.stats.length === 1) {
while ((existingPhase = (this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex && p.stats.length === 1
&& (p.stats[0] === this.stats[0] || (random && p.stats[0] === BattleStat.RAND))
&& p.selfTarget === this.selfTarget && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatChangePhase))) {
if (existingPhase.stats[0] === BattleStat.RAND) {
existingPhase.stats[0] = this.getRandomStat();
if (existingPhase.stats[0] !== this.stats[0]) {
continue;
}
}
this.levels += existingPhase.levels;
if (!this.scene.tryRemovePhase(p => p === existingPhase)) {
break;
}
}
}
while ((existingPhase = (this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex && p.selfTarget === this.selfTarget
&& ([BattleStat.ACC, BattleStat.EVA].some(s => p.stats.includes(s)) === isAccEva)
&& p.levels === this.levels && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatChangePhase))) {
this.stats.push(...existingPhase.stats);
if (!this.scene.tryRemovePhase(p => p === existingPhase)) {
break;
}
}
}
getStatChangeMessages(stats: BattleStat[], levels: integer, relLevels: integer[]): string[] {
const messages: string[] = [];
const relLevelStatIndexes = {};
for (let rl = 0; rl < relLevels.length; rl++) {
const relLevel = relLevels[rl];
if (!relLevelStatIndexes[relLevel]) {
relLevelStatIndexes[relLevel] = [];
}
relLevelStatIndexes[relLevel].push(rl);
}
Object.keys(relLevelStatIndexes).forEach(rl => {
const relLevelStats = stats.filter((_, i) => relLevelStatIndexes[rl].includes(i));
let statsFragment = "";
if (relLevelStats.length > 1) {
statsFragment = relLevelStats.length >= 5
? i18next.t("battle:stats")
: `${relLevelStats.slice(0, -1).map(s => getBattleStatName(s)).join(", ")}${relLevelStats.length > 2 ? "," : ""} ${i18next.t("battle:statsAnd")} ${getBattleStatName(relLevelStats[relLevelStats.length - 1])}`;
messages.push(getBattleStatLevelChangeDescription(getPokemonNameWithAffix(this.getPokemon()), statsFragment, Math.abs(parseInt(rl)), levels >= 1, relLevelStats.length));
} else {
statsFragment = getBattleStatName(relLevelStats[0]);
messages.push(getBattleStatLevelChangeDescription(getPokemonNameWithAffix(this.getPokemon()), statsFragment, Math.abs(parseInt(rl)), levels >= 1, relLevelStats.length));
}
});
return messages;
}
}

View File

@ -0,0 +1,244 @@
import { BattlerIndex } from "#app/battle";
import BattleScene from "#app/battle-scene";
import { applyAbAttrs, applyPostStatStageChangeAbAttrs, applyPreStatStageChangeAbAttrs, PostStatStageChangeAbAttr, ProtectStatAbAttr, StatStageChangeCopyAbAttr, StatStageChangeMultiplierAbAttr } from "#app/data/ability";
import { ArenaTagSide, MistTag } from "#app/data/arena-tag";
import Pokemon from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { ResetNegativeStatStageModifier } from "#app/modifier/modifier";
import { handleTutorial, Tutorial } from "#app/tutorial";
import * as Utils from "#app/utils";
import i18next from "i18next";
import { PokemonPhase } from "./pokemon-phase";
import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat";
export type StatStageChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void;
export class StatStageChangePhase extends PokemonPhase {
private stats: BattleStat[];
private selfTarget: boolean;
private stages: integer;
private showMessage: boolean;
private ignoreAbilities: boolean;
private canBeCopied: boolean;
private onChange: StatStageChangeCallback | null;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], stages: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatStageChangeCallback | null = null) {
super(scene, battlerIndex);
this.selfTarget = selfTarget;
this.stats = stats;
this.stages = stages;
this.showMessage = showMessage;
this.ignoreAbilities = ignoreAbilities;
this.canBeCopied = canBeCopied;
this.onChange = onChange;
}
start() {
const pokemon = this.getPokemon();
if (!pokemon.isActive(true)) {
return this.end();
}
let simulate = false;
const filteredStats = this.stats.filter(stat => {
const cancelled = new Utils.BooleanHolder(false);
if (!this.selfTarget && this.stages < 0) {
// TODO: Include simulate boolean when tag applications can be simulated
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled);
}
if (!cancelled.value && !this.selfTarget && this.stages < 0) {
applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate);
}
// If one stat stage decrease is cancelled, simulate the rest of the applications
if (cancelled.value) {
simulate = true;
}
return !cancelled.value;
});
const stages = new Utils.IntegerHolder(this.stages);
if (!this.ignoreAbilities) {
applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages);
}
const relLevels = filteredStats.map(s => (stages.value >= 1 ? Math.min(pokemon.getStatStage(s) + stages.value, 6) : Math.max(pokemon.getStatStage(s) + stages.value, -6)) - pokemon.getStatStage(s));
this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels);
const end = () => {
if (this.showMessage) {
const messages = this.getStatStageChangeMessages(filteredStats, stages.value, relLevels);
for (const message of messages) {
this.scene.queueMessage(message);
}
}
for (const s of filteredStats) {
if (stages.value > 0 && pokemon.getStatStage(s) < 6) {
if (!pokemon.turnData) {
// Temporary fix for missing turn data struct on turn 1
pokemon.resetTurnData();
}
pokemon.turnData.statStagesIncreased = true;
} else if (stages.value < 0 && pokemon.getStatStage(s) > -6) {
if (!pokemon.turnData) {
// Temporary fix for missing turn data struct on turn 1
pokemon.resetTurnData();
}
pokemon.turnData.statStagesDecreased = true;
}
pokemon.setStatStage(s, pokemon.getStatStage(s) + stages.value);
}
if (stages.value > 0 && this.canBeCopied) {
for (const opponent of pokemon.getOpponents()) {
applyAbAttrs(StatStageChangeCopyAbAttr, opponent, null, false, this.stats, stages.value);
}
}
applyPostStatStageChangeAbAttrs(PostStatStageChangeAbAttr, pokemon, filteredStats, this.stages, this.selfTarget);
// Look for any other stat change phases; if this is the last one, do White Herb check
const existingPhase = this.scene.findPhase(p => p instanceof StatStageChangePhase && p.battlerIndex === this.battlerIndex);
if (!(existingPhase instanceof StatStageChangePhase)) {
// Apply White Herb if needed
const whiteHerb = this.scene.applyModifier(ResetNegativeStatStageModifier, this.player, pokemon) as ResetNegativeStatStageModifier;
// If the White Herb was applied, consume it
if (whiteHerb) {
whiteHerb.stackCount--;
if (whiteHerb.stackCount <= 0) {
this.scene.removeModifier(whiteHerb);
}
this.scene.updateModifiers(this.player);
}
}
pokemon.updateInfo();
handleTutorial(this.scene, Tutorial.Stat_Change).then(() => super.end());
};
if (relLevels.filter(l => l).length && this.scene.moveAnimations) {
pokemon.enableMask();
const pokemonMaskSprite = pokemon.maskSprite;
const tileX = (this.player ? 106 : 236) * pokemon.getSpriteScale() * this.scene.field.scale;
const tileY = ((this.player ? 148 : 84) + (stages.value >= 1 ? 160 : 0)) * pokemon.getSpriteScale() * this.scene.field.scale;
const tileWidth = 156 * this.scene.field.scale * pokemon.getSpriteScale();
const tileHeight = 316 * this.scene.field.scale * pokemon.getSpriteScale();
// On increase, show the red sprite located at ATK
// On decrease, show the blue sprite located at SPD
const spriteColor = stages.value >= 1 ? Stat[Stat.ATK].toLowerCase() : Stat[Stat.SPD].toLowerCase();
const statSprite = this.scene.add.tileSprite(tileX, tileY, tileWidth, tileHeight, "battle_stats", spriteColor);
statSprite.setPipeline(this.scene.fieldSpritePipeline);
statSprite.setAlpha(0);
statSprite.setScale(6);
statSprite.setOrigin(0.5, 1);
this.scene.playSound(`se/stat_${stages.value >= 1 ? "up" : "down"}`);
statSprite.setMask(new Phaser.Display.Masks.BitmapMask(this.scene, pokemonMaskSprite ?? undefined));
this.scene.tweens.add({
targets: statSprite,
duration: 250,
alpha: 0.8375,
onComplete: () => {
this.scene.tweens.add({
targets: statSprite,
delay: 1000,
duration: 250,
alpha: 0
});
}
});
this.scene.tweens.add({
targets: statSprite,
duration: 1500,
y: `${stages.value >= 1 ? "-" : "+"}=${160 * 6}`
});
this.scene.time.delayedCall(1750, () => {
pokemon.disableMask();
end();
});
} else {
end();
}
}
aggregateStatStageChanges(): void {
const accEva: BattleStat[] = [ Stat.ACC, Stat.EVA ];
const isAccEva = accEva.some(s => this.stats.includes(s));
let existingPhase: StatStageChangePhase;
if (this.stats.length === 1) {
while ((existingPhase = (this.scene.findPhase(p => p instanceof StatStageChangePhase && p.battlerIndex === this.battlerIndex && p.stats.length === 1
&& (p.stats[0] === this.stats[0])
&& p.selfTarget === this.selfTarget && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatStageChangePhase))) {
this.stages += existingPhase.stages;
if (!this.scene.tryRemovePhase(p => p === existingPhase)) {
break;
}
}
}
while ((existingPhase = (this.scene.findPhase(p => p instanceof StatStageChangePhase && p.battlerIndex === this.battlerIndex && p.selfTarget === this.selfTarget
&& (accEva.some(s => p.stats.includes(s)) === isAccEva)
&& p.stages === this.stages && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatStageChangePhase))) {
this.stats.push(...existingPhase.stats);
if (!this.scene.tryRemovePhase(p => p === existingPhase)) {
break;
}
}
}
getStatStageChangeMessages(stats: BattleStat[], stages: integer, relStages: integer[]): string[] {
const messages: string[] = [];
const relStageStatIndexes = {};
for (let rl = 0; rl < relStages.length; rl++) {
const relStage = relStages[rl];
if (!relStageStatIndexes[relStage]) {
relStageStatIndexes[relStage] = [];
}
relStageStatIndexes[relStage].push(rl);
}
Object.keys(relStageStatIndexes).forEach(rl => {
const relStageStats = stats.filter((_, i) => relStageStatIndexes[rl].includes(i));
let statsFragment = "";
if (relStageStats.length > 1) {
statsFragment = relStageStats.length >= 5
? i18next.t("battle:stats")
: `${relStageStats.slice(0, -1).map(s => i18next.t(getStatKey(s))).join(", ")}${relStageStats.length > 2 ? "," : ""} ${i18next.t("battle:statsAnd")} ${i18next.t(getStatKey(relStageStats[relStageStats.length - 1]))}`;
messages.push(i18next.t(getStatStageChangeDescriptionKey(Math.abs(parseInt(rl)), stages >= 1), {
pokemonNameWithAffix: getPokemonNameWithAffix(this.getPokemon()),
stats: statsFragment,
count: relStageStats.length
}));
} else {
statsFragment = i18next.t(getStatKey(relStageStats[0]));
messages.push(i18next.t(getStatStageChangeDescriptionKey(Math.abs(parseInt(rl)), stages >= 1), {
pokemonNameWithAffix: getPokemonNameWithAffix(this.getPokemon()),
stats: statsFragment,
count: relStageStats.length
}));
}
});
return messages;
}
}

View File

@ -43,8 +43,8 @@ export class TurnStartPhase extends FieldPhase {
}, this.scene.currentBattle.turn, this.scene.waveSeed); }, this.scene.currentBattle.turn, this.scene.waveSeed);
orderedTargets.sort((a: Pokemon, b: Pokemon) => { orderedTargets.sort((a: Pokemon, b: Pokemon) => {
const aSpeed = a?.getBattleStat(Stat.SPD) || 0; const aSpeed = a?.getEffectiveStat(Stat.SPD) || 0;
const bSpeed = b?.getBattleStat(Stat.SPD) || 0; const bSpeed = b?.getEffectiveStat(Stat.SPD) || 0;
return bSpeed - aSpeed; return bSpeed - aSpeed;
}); });

View File

@ -5,9 +5,10 @@ import { pokemonEvolutions } from "#app/data/pokemon-evolutions";
import i18next from "i18next"; import i18next from "i18next";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { Challenge, FreshStartChallenge, InverseBattleChallenge, SingleGenerationChallenge, SingleTypeChallenge } from "#app/data/challenge"; import { Challenge, FreshStartChallenge, SingleGenerationChallenge, SingleTypeChallenge, InverseBattleChallenge } from "#app/data/challenge";
import { Challenges } from "#app/enums/challenges";
import { ConditionFn } from "#app/@types/common"; import { ConditionFn } from "#app/@types/common";
import { Stat, getShortenedStatKey } from "#app/enums/stat";
import { Challenges } from "#app/enums/challenges";
export enum AchvTier { export enum AchvTier {
COMMON, COMMON,
@ -172,13 +173,13 @@ export function getAchievementDescription(localizationKey: string): string {
case "10000_DMG": case "10000_DMG":
return i18next.t("achv:DamageAchv.description", {context: genderStr, "damageAmount": achvs._10000_DMG.damageAmount.toLocaleString("en-US")}); return i18next.t("achv:DamageAchv.description", {context: genderStr, "damageAmount": achvs._10000_DMG.damageAmount.toLocaleString("en-US")});
case "250_HEAL": case "250_HEAL":
return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._250_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")}); return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._250_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP))});
case "1000_HEAL": case "1000_HEAL":
return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._1000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")}); return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._1000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP))});
case "2500_HEAL": case "2500_HEAL":
return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._2500_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")}); return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._2500_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP))});
case "10000_HEAL": case "10000_HEAL":
return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._10000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t("pokemonInfo:Stat.HPshortened")}); return i18next.t("achv:HealAchv.description", {context: genderStr, "healAmount": achvs._10000_HEAL.healAmount.toLocaleString("en-US"), "HP": i18next.t(getShortenedStatKey(Stat.HP))});
case "LV_100": case "LV_100":
return i18next.t("achv:LevelAchv.description", {context: genderStr, "level": achvs.LV_100.level}); return i18next.t("achv:LevelAchv.description", {context: genderStr, "level": achvs.LV_100.level});
case "LV_250": case "LV_250":
@ -195,7 +196,7 @@ export function getAchievementDescription(localizationKey: string): string {
return i18next.t("achv:RibbonAchv.description", {context: genderStr, "ribbonAmount": achvs._75_RIBBONS.ribbonAmount.toLocaleString("en-US")}); return i18next.t("achv:RibbonAchv.description", {context: genderStr, "ribbonAmount": achvs._75_RIBBONS.ribbonAmount.toLocaleString("en-US")});
case "100_RIBBONS": case "100_RIBBONS":
return i18next.t("achv:RibbonAchv.description", {context: genderStr, "ribbonAmount": achvs._100_RIBBONS.ribbonAmount.toLocaleString("en-US")}); return i18next.t("achv:RibbonAchv.description", {context: genderStr, "ribbonAmount": achvs._100_RIBBONS.ribbonAmount.toLocaleString("en-US")});
case "TRANSFER_MAX_BATTLE_STAT": case "TRANSFER_MAX_STAT_STAGE":
return i18next.t("achv:TRANSFER_MAX_BATTLE_STAT.description", { context: genderStr }); return i18next.t("achv:TRANSFER_MAX_BATTLE_STAT.description", { context: genderStr });
case "MAX_FRIENDSHIP": case "MAX_FRIENDSHIP":
return i18next.t("achv:MAX_FRIENDSHIP.description", { context: genderStr }); return i18next.t("achv:MAX_FRIENDSHIP.description", { context: genderStr });
@ -305,7 +306,7 @@ export const achvs = {
_50_RIBBONS: new RibbonAchv("50_RIBBONS", "", 50, "ultra_ribbon", 50).setSecret(true), _50_RIBBONS: new RibbonAchv("50_RIBBONS", "", 50, "ultra_ribbon", 50).setSecret(true),
_75_RIBBONS: new RibbonAchv("75_RIBBONS", "", 75, "rogue_ribbon", 75).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), _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), MAX_FRIENDSHIP: new Achv("MAX_FRIENDSHIP", "", "MAX_FRIENDSHIP.description", "soothe_bell", 25),
MEGA_EVOLVE: new Achv("MEGA_EVOLVE", "", "MEGA_EVOLVE.description", "mega_bracelet", 50), MEGA_EVOLVE: new Achv("MEGA_EVOLVE", "", "MEGA_EVOLVE.description", "mega_bracelet", 50),
GIGANTAMAX: new Achv("GIGANTAMAX", "", "GIGANTAMAX.description", "dynamax_band", 50), GIGANTAMAX: new Achv("GIGANTAMAX", "", "GIGANTAMAX.description", "dynamax_band", 50),

View File

@ -124,7 +124,8 @@ export default class PokemonData {
this.summonData = new PokemonSummonData(); this.summonData = new PokemonSummonData();
if (!forHistory && source.summonData) { if (!forHistory && source.summonData) {
this.summonData.battleStats = source.summonData.battleStats; this.summonData.stats = source.summonData.stats;
this.summonData.statStages = source.summonData.statStages;
this.summonData.moveQueue = source.summonData.moveQueue; this.summonData.moveQueue = source.summonData.moveQueue;
this.summonData.disabledMove = source.summonData.disabledMove; this.summonData.disabledMove = source.summonData.disabledMove;
this.summonData.disabledTurns = source.summonData.disabledTurns; this.summonData.disabledTurns = source.summonData.disabledTurns;

View File

@ -0,0 +1,97 @@
import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
import { VictoryPhase } from "#app/phases/victory-phase";
import { TurnStartPhase } from "#app/phases/turn-start-phase";
import { BattlerIndex } from "#app/battle";
describe("Abilities - Beast Boost", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.enemySpecies(Species.BULBASAUR)
.enemyAbility(Abilities.BEAST_BOOST)
.ability(Abilities.BEAST_BOOST)
.startingLevel(2000)
.moveset([ Moves.FLAMETHROWER ])
.enemyMoveset(SPLASH_ONLY);
});
// Note that both MOXIE and BEAST_BOOST test for their current implementation and not their mainline behavior.
it("should prefer highest stat to boost its corresponding stat stage by 1 when winning a battle", async() => {
// SLOWBRO's highest stat is DEF, so it should be picked here
await game.startBattle([
Species.SLOWBRO
]);
const playerPokemon = game.scene.getPlayerPokemon()!;
expect(playerPokemon.getStatStage(Stat.DEF)).toBe(0);
game.move.select(Moves.FLAMETHROWER);
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase);
expect(playerPokemon.getStatStage(Stat.DEF)).toBe(1);
}, 20000);
it("should use in-battle overriden stats when determining the stat stage to raise by 1", async() => {
// If the opponent can GUARD_SPLIT, SLOWBRO's second highest stat should be SPATK
game.override.enemyMoveset(new Array(4).fill(Moves.GUARD_SPLIT));
await game.startBattle([
Species.SLOWBRO
]);
const playerPokemon = game.scene.getPlayerPokemon()!;
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(0);
game.move.select(Moves.FLAMETHROWER);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.runFrom(TurnStartPhase).to(VictoryPhase);
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
}, 20000);
it("should have order preference in case of stat ties", async() => {
// Order preference follows the order of EFFECTIVE_STAT
await game.startBattle([
Species.SLOWBRO
]);
const playerPokemon = game.scene.getPlayerPokemon()!;
// Set up tie between SPATK, SPDEF, and SPD, where SPATK should win
vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([ 10000, 1, 1, 100, 100, 100 ]);
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(0);
game.move.select(Moves.FLAMETHROWER);
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase);
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
}, 20000);
});

View File

@ -0,0 +1,42 @@
import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
describe("Abilities - Contrary", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.enemySpecies(Species.BULBASAUR)
.enemyAbility(Abilities.CONTRARY)
.ability(Abilities.INTIMIDATE)
.enemyMoveset(SPLASH_ONLY);
});
it("should invert stat changes when applied", async() => {
await game.startBattle([
Species.SLOWBRO
]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
}, 20000);
});

View File

@ -1,4 +1,4 @@
import { BattleStat } from "#app/data/battle-stat"; import { Stat } from "#enums/stat";
import { Abilities } from "#app/enums/abilities"; import { Abilities } from "#app/enums/abilities";
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species"; import { Species } from "#app/enums/species";
@ -35,7 +35,7 @@ describe("Abilities - COSTAR", () => {
test( test(
"ability copies positive stat changes", "ability copies positive stat stages",
async () => { async () => {
game.override.enemyAbility(Abilities.BALL_FETCH); game.override.enemyAbility(Abilities.BALL_FETCH);
@ -48,8 +48,8 @@ describe("Abilities - COSTAR", () => {
game.move.select(Moves.SPLASH, 1); game.move.select(Moves.SPLASH, 1);
await game.toNextTurn(); await game.toNextTurn();
expect(leftPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(+2); expect(leftPokemon.getStatStage(Stat.SPATK)).toBe(2);
expect(rightPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(0); expect(rightPokemon.getStatStage(Stat.SPATK)).toBe(0);
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(CommandPhase); await game.phaseInterceptor.to(CommandPhase);
@ -57,14 +57,14 @@ describe("Abilities - COSTAR", () => {
await game.phaseInterceptor.to(MessagePhase); await game.phaseInterceptor.to(MessagePhase);
[leftPokemon, rightPokemon] = game.scene.getPlayerField(); [leftPokemon, rightPokemon] = game.scene.getPlayerField();
expect(leftPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(+2); expect(leftPokemon.getStatStage(Stat.SPATK)).toBe(2);
expect(rightPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(+2); expect(rightPokemon.getStatStage(Stat.SPATK)).toBe(2);
}, },
TIMEOUT, TIMEOUT,
); );
test( test(
"ability copies negative stat changes", "ability copies negative stat stages",
async () => { async () => {
game.override.enemyAbility(Abilities.INTIMIDATE); game.override.enemyAbility(Abilities.INTIMIDATE);
@ -72,8 +72,8 @@ describe("Abilities - COSTAR", () => {
let [leftPokemon, rightPokemon] = game.scene.getPlayerField(); let [leftPokemon, rightPokemon] = game.scene.getPlayerField();
expect(leftPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2); expect(leftPokemon.getStatStage(Stat.ATK)).toBe(-2);
expect(leftPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2); expect(leftPokemon.getStatStage(Stat.ATK)).toBe(-2);
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(CommandPhase); await game.phaseInterceptor.to(CommandPhase);
@ -81,8 +81,8 @@ describe("Abilities - COSTAR", () => {
await game.phaseInterceptor.to(MessagePhase); await game.phaseInterceptor.to(MessagePhase);
[leftPokemon, rightPokemon] = game.scene.getPlayerField(); [leftPokemon, rightPokemon] = game.scene.getPlayerField();
expect(leftPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2); expect(leftPokemon.getStatStage(Stat.ATK)).toBe(-2);
expect(rightPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2); expect(rightPokemon.getStatStage(Stat.ATK)).toBe(-2);
}, },
TIMEOUT, TIMEOUT,
); );

View File

@ -1,8 +1,8 @@
import { BattleStat } from "#app/data/battle-stat";
import { StatusEffect } from "#app/data/status-effect";
import { toDmgValue } from "#app/utils"; import { toDmgValue } from "#app/utils";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { StatusEffect } from "#app/data/status-effect";
import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils"; import { SPLASH_ONLY } from "../utils/testUtils";
@ -36,7 +36,7 @@ describe("Abilities - Disguise", () => {
}, TIMEOUT); }, TIMEOUT);
it("takes no damage from attacking move and transforms to Busted form, takes 1/8 max HP damage from the disguise breaking", async () => { it("takes no damage from attacking move and transforms to Busted form, takes 1/8 max HP damage from the disguise breaking", async () => {
await game.startBattle(); await game.classicMode.startBattle();
const mimikyu = game.scene.getEnemyPokemon()!; const mimikyu = game.scene.getEnemyPokemon()!;
const maxHp = mimikyu.getMaxHp(); const maxHp = mimikyu.getMaxHp();
@ -53,7 +53,7 @@ describe("Abilities - Disguise", () => {
}, TIMEOUT); }, TIMEOUT);
it("doesn't break disguise when attacked with ineffective move", async () => { it("doesn't break disguise when attacked with ineffective move", async () => {
await game.startBattle(); await game.classicMode.startBattle();
const mimikyu = game.scene.getEnemyPokemon()!; const mimikyu = game.scene.getEnemyPokemon()!;
@ -67,9 +67,9 @@ describe("Abilities - Disguise", () => {
}, TIMEOUT); }, TIMEOUT);
it("takes no damage from the first hit of a multihit move and transforms to Busted form, then takes damage from the second hit", async () => { it("takes no damage from the first hit of a multihit move and transforms to Busted form, then takes damage from the second hit", async () => {
game.override.moveset([Moves.SURGING_STRIKES]); game.override.moveset([ Moves.SURGING_STRIKES ]);
game.override.enemyLevel(5); game.override.enemyLevel(5);
await game.startBattle(); await game.classicMode.startBattle();
const mimikyu = game.scene.getEnemyPokemon()!; const mimikyu = game.scene.getEnemyPokemon()!;
const maxHp = mimikyu.getMaxHp(); const maxHp = mimikyu.getMaxHp();
@ -91,7 +91,7 @@ describe("Abilities - Disguise", () => {
}, TIMEOUT); }, TIMEOUT);
it("takes effects from status moves and damage from status effects", async () => { it("takes effects from status moves and damage from status effects", async () => {
await game.startBattle(); await game.classicMode.startBattle();
const mimikyu = game.scene.getEnemyPokemon()!; const mimikyu = game.scene.getEnemyPokemon()!;
expect(mimikyu.hp).toBe(mimikyu.getMaxHp()); expect(mimikyu.hp).toBe(mimikyu.getMaxHp());
@ -102,7 +102,7 @@ describe("Abilities - Disguise", () => {
expect(mimikyu.formIndex).toBe(disguisedForm); expect(mimikyu.formIndex).toBe(disguisedForm);
expect(mimikyu.status?.effect).toBe(StatusEffect.POISON); expect(mimikyu.status?.effect).toBe(StatusEffect.POISON);
expect(mimikyu.summonData.battleStats[BattleStat.SPD]).toBe(-1); expect(mimikyu.getStatStage(Stat.SPD)).toBe(-1);
expect(mimikyu.hp).toBeLessThan(mimikyu.getMaxHp()); expect(mimikyu.hp).toBeLessThan(mimikyu.getMaxHp());
}, TIMEOUT); }, TIMEOUT);
@ -110,7 +110,7 @@ describe("Abilities - Disguise", () => {
game.override.enemyMoveset(Array(4).fill(Moves.SHADOW_SNEAK)); game.override.enemyMoveset(Array(4).fill(Moves.SHADOW_SNEAK));
game.override.starterSpecies(0); game.override.starterSpecies(0);
await game.startBattle([Species.MIMIKYU, Species.FURRET]); await game.classicMode.startBattle([ Species.MIMIKYU, Species.FURRET ]);
const mimikyu = game.scene.getPlayerPokemon()!; const mimikyu = game.scene.getPlayerPokemon()!;
const maxHp = mimikyu.getMaxHp(); const maxHp = mimikyu.getMaxHp();
@ -136,7 +136,7 @@ describe("Abilities - Disguise", () => {
game.override.starterForms({ game.override.starterForms({
[Species.MIMIKYU]: bustedForm [Species.MIMIKYU]: bustedForm
}); });
await game.startBattle([Species.FURRET, Species.MIMIKYU]); await game.classicMode.startBattle([ Species.FURRET, Species.MIMIKYU ]);
const mimikyu = game.scene.getParty()[1]!; const mimikyu = game.scene.getParty()[1]!;
expect(mimikyu.formIndex).toBe(bustedForm); expect(mimikyu.formIndex).toBe(bustedForm);
@ -155,7 +155,7 @@ describe("Abilities - Disguise", () => {
[Species.MIMIKYU]: bustedForm [Species.MIMIKYU]: bustedForm
}); });
await game.startBattle(); await game.classicMode.startBattle();
const mimikyu = game.scene.getPlayerPokemon()!; const mimikyu = game.scene.getPlayerPokemon()!;
@ -175,7 +175,7 @@ describe("Abilities - Disguise", () => {
[Species.MIMIKYU]: bustedForm [Species.MIMIKYU]: bustedForm
}); });
await game.startBattle([Species.MIMIKYU, Species.FURRET]); await game.classicMode.startBattle([ Species.MIMIKYU, Species.FURRET ]);
const mimikyu1 = game.scene.getPlayerPokemon()!; const mimikyu1 = game.scene.getPlayerPokemon()!;
@ -194,7 +194,7 @@ describe("Abilities - Disguise", () => {
it("doesn't faint twice when fainting due to Disguise break damage, nor prevent faint from Disguise break damage if using Endure", async () => { it("doesn't faint twice when fainting due to Disguise break damage, nor prevent faint from Disguise break damage if using Endure", async () => {
game.override.enemyMoveset(Array(4).fill(Moves.ENDURE)); game.override.enemyMoveset(Array(4).fill(Moves.ENDURE));
await game.startBattle(); await game.classicMode.startBattle();
const mimikyu = game.scene.getEnemyPokemon()!; const mimikyu = game.scene.getEnemyPokemon()!;
mimikyu.hp = 1; mimikyu.hp = 1;

View File

@ -49,16 +49,16 @@ describe("Abilities - Flower Gift", () => {
}); });
// TODO: Uncomment expect statements when the ability is implemented - currently does not increase stats of allies // TODO: Uncomment expect statements when the ability is implemented - currently does not increase stats of allies
it("increases the Attack and Special Defense stats of the Pokémon with this Ability and its allies by 1.5× during Harsh Sunlight", async () => { it("increases the ATK and SPDEF stat stages of the Pokémon with this Ability and its allies by 1.5× during Harsh Sunlight", async () => {
game.override.battleType("double"); game.override.battleType("double");
await game.classicMode.startBattle([Species.CHERRIM, Species.MAGIKARP]); await game.classicMode.startBattle([Species.CHERRIM, Species.MAGIKARP]);
const [ cherrim ] = game.scene.getPlayerField(); const [ cherrim ] = game.scene.getPlayerField();
const cherrimAtkStat = cherrim.getBattleStat(Stat.ATK); const cherrimAtkStat = cherrim.getEffectiveStat(Stat.ATK);
const cherrimSpDefStat = cherrim.getBattleStat(Stat.SPDEF); const cherrimSpDefStat = cherrim.getEffectiveStat(Stat.SPDEF);
// const magikarpAtkStat = magikarp.getBattleStat(Stat.ATK);; // const magikarpAtkStat = magikarp.getEffectiveStat(Stat.ATK);;
// const magikarpSpDefStat = magikarp.getBattleStat(Stat.SPDEF); // const magikarpSpDefStat = magikarp.getEffectiveStat(Stat.SPDEF);
game.move.select(Moves.SUNNY_DAY, 0); game.move.select(Moves.SUNNY_DAY, 0);
game.move.select(Moves.SPLASH, 1); game.move.select(Moves.SPLASH, 1);
@ -67,10 +67,10 @@ describe("Abilities - Flower Gift", () => {
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
expect(cherrim.formIndex).toBe(SUNSHINE_FORM); expect(cherrim.formIndex).toBe(SUNSHINE_FORM);
expect(cherrim.getBattleStat(Stat.ATK)).toBe(Math.floor(cherrimAtkStat * 1.5)); expect(cherrim.getEffectiveStat(Stat.ATK)).toBe(Math.floor(cherrimAtkStat * 1.5));
expect(cherrim.getBattleStat(Stat.SPDEF)).toBe(Math.floor(cherrimSpDefStat * 1.5)); expect(cherrim.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(cherrimSpDefStat * 1.5));
// expect(magikarp.getBattleStat(Stat.ATK)).toBe(Math.floor(magikarpAtkStat * 1.5)); // expect(magikarp.getEffectiveStat(Stat.ATK)).toBe(Math.floor(magikarpAtkStat * 1.5));
// expect(magikarp.getBattleStat(Stat.SPDEF)).toBe(Math.floor(magikarpSpDefStat * 1.5)); // expect(magikarp.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(magikarpSpDefStat * 1.5));
}); });
it("changes the Pokemon's form during Harsh Sunlight", async () => { it("changes the Pokemon's form during Harsh Sunlight", async () => {

View File

@ -1,4 +1,3 @@
import { BattleStat } from "#app/data/battle-stat";
import { BattlerTagType } from "#app/enums/battler-tag-type"; import { BattlerTagType } from "#app/enums/battler-tag-type";
import { StatusEffect } from "#app/enums/status-effect"; import { StatusEffect } from "#app/enums/status-effect";
import Pokemon from "#app/field/pokemon"; import Pokemon from "#app/field/pokemon";
@ -13,6 +12,7 @@ import { Species } from "#enums/species";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils"; import { SPLASH_ONLY } from "../utils/testUtils";
import { Stat } from "#enums/stat";
describe("Abilities - Gulp Missile", () => { describe("Abilities - Gulp Missile", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -107,7 +107,7 @@ describe("Abilities - Gulp Missile", () => {
expect(cramorant.formIndex).toBe(GULPING_FORM); 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)); game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
await game.startBattle([Species.CRAMORANT]); await game.startBattle([Species.CRAMORANT]);
@ -139,7 +139,7 @@ describe("Abilities - Gulp Missile", () => {
expect(cramorant.formIndex).toBe(GULPING_FORM); 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)); game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
await game.startBattle([Species.CRAMORANT]); await game.startBattle([Species.CRAMORANT]);
@ -158,7 +158,7 @@ describe("Abilities - Gulp Missile", () => {
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy)); 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.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeUndefined();
expect(cramorant.formIndex).toBe(NORMAL_FORM); expect(cramorant.formIndex).toBe(NORMAL_FORM);
}); });
@ -219,7 +219,7 @@ describe("Abilities - Gulp Missile", () => {
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(enemy.hp).toBe(enemyHpPreEffect); 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.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeUndefined();
expect(cramorant.formIndex).toBe(NORMAL_FORM); expect(cramorant.formIndex).toBe(NORMAL_FORM);
}); });

View File

@ -26,7 +26,7 @@ describe("Abilities - Hustle", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
.ability(Abilities.HUSTLE) .ability(Abilities.HUSTLE)
.moveset([Moves.TACKLE, Moves.GIGA_DRAIN, Moves.FISSURE]) .moveset([ Moves.TACKLE, Moves.GIGA_DRAIN, Moves.FISSURE ])
.disableCrits() .disableCrits()
.battleType("single") .battleType("single")
.enemyMoveset(SPLASH_ONLY) .enemyMoveset(SPLASH_ONLY)
@ -39,13 +39,13 @@ describe("Abilities - Hustle", () => {
const pikachu = game.scene.getPlayerPokemon()!; const pikachu = game.scene.getPlayerPokemon()!;
const atk = pikachu.stats[Stat.ATK]; const atk = pikachu.stats[Stat.ATK];
vi.spyOn(pikachu, "getBattleStat"); vi.spyOn(pikachu, "getEffectiveStat");
game.move.select(Moves.TACKLE); game.move.select(Moves.TACKLE);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamagePhase");
expect(pikachu.getBattleStat).toHaveReturnedWith(Math.floor(atk * 1.5)); expect(pikachu.getEffectiveStat).toHaveReturnedWith(Math.floor(atk * 1.5));
}); });
it("lowers the accuracy of the user's physical moves by 20%", async () => { it("lowers the accuracy of the user's physical moves by 20%", async () => {
@ -65,13 +65,13 @@ describe("Abilities - Hustle", () => {
const pikachu = game.scene.getPlayerPokemon()!; const pikachu = game.scene.getPlayerPokemon()!;
const spatk = pikachu.stats[Stat.SPATK]; const spatk = pikachu.stats[Stat.SPATK];
vi.spyOn(pikachu, "getBattleStat"); vi.spyOn(pikachu, "getEffectiveStat");
vi.spyOn(pikachu, "getAccuracyMultiplier"); vi.spyOn(pikachu, "getAccuracyMultiplier");
game.move.select(Moves.GIGA_DRAIN); game.move.select(Moves.GIGA_DRAIN);
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamagePhase");
expect(pikachu.getBattleStat).toHaveReturnedWith(spatk); expect(pikachu.getEffectiveStat).toHaveReturnedWith(spatk);
expect(pikachu.getAccuracyMultiplier).toHaveReturnedWith(1); expect(pikachu.getAccuracyMultiplier).toHaveReturnedWith(1);
}); });

View File

@ -1,4 +1,4 @@
import { BattleStat } from "#app/data/battle-stat"; import { Stat } from "#enums/stat";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
@ -51,7 +51,7 @@ describe("Abilities - Hyper Cutter", () => {
game.move.select(Moves.STRING_SHOT); game.move.select(Moves.STRING_SHOT);
await game.toNextTurn(); await game.toNextTurn();
expect(enemy.summonData.battleStats[BattleStat.ATK]).toEqual(0); expect(enemy.getStatStage(Stat.ATK)).toEqual(0);
[BattleStat.ACC, BattleStat.DEF, BattleStat.EVA, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD].forEach((stat: number) => expect(enemy.summonData.battleStats[stat]).toBeLessThan(0)); [Stat.ACC, Stat.DEF, Stat.EVA, Stat.SPATK, Stat.SPDEF, Stat.SPD].forEach((stat: number) => expect(enemy.getStatStage(stat)).toBeLessThan(0));
}); });
}); });

View File

@ -0,0 +1,101 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import { Species } from "#enums/species";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
import { Stat, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
import { Abilities } from "#enums/abilities";
import { SPLASH_ONLY } from "../utils/testUtils";
// TODO: Add more tests once Imposter is fully implemented
describe("Abilities - Imposter", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.enemySpecies(Species.MEW)
.enemyLevel(200)
.enemyAbility(Abilities.BEAST_BOOST)
.enemyPassiveAbility(Abilities.BALL_FETCH)
.enemyMoveset(SPLASH_ONLY)
.ability(Abilities.IMPOSTER)
.moveset(SPLASH_ONLY);
});
it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => {
await game.startBattle([
Species.DITTO
]);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
expect(player.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId);
expect(player.getAbility()).toBe(enemy.getAbility());
expect(player.getGender()).toBe(enemy.getGender());
expect(player.getStat(Stat.HP, false)).not.toBe(enemy.getStat(Stat.HP));
for (const s of EFFECTIVE_STATS) {
expect(player.getStat(s, false)).toBe(enemy.getStat(s, false));
}
for (const s of BATTLE_STATS) {
expect(player.getStatStage(s)).toBe(enemy.getStatStage(s));
}
const playerMoveset = player.getMoveset();
const enemyMoveset = player.getMoveset();
for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) {
// TODO: Checks for 5 PP should be done here when that gets addressed
expect(playerMoveset[i]?.moveId).toBe(enemyMoveset[i]?.moveId);
}
const playerTypes = player.getTypes();
const enemyTypes = enemy.getTypes();
for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) {
expect(playerTypes[i]).toBe(enemyTypes[i]);
}
}, 20000);
it("should copy in-battle overridden stats", async () => {
game.override.enemyMoveset(new Array(4).fill(Moves.POWER_SPLIT));
await game.startBattle([
Species.DITTO
]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
const avgAtk = Math.floor((player.getStat(Stat.ATK, false) + enemy.getStat(Stat.ATK, false)) / 2);
const avgSpAtk = Math.floor((player.getStat(Stat.SPATK, false) + enemy.getStat(Stat.SPATK, false)) / 2);
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to(TurnEndPhase);
expect(player.getStat(Stat.ATK, false)).toBe(avgAtk);
expect(enemy.getStat(Stat.ATK, false)).toBe(avgAtk);
expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
});
});

View File

@ -1,17 +1,13 @@
import { BattleStat } from "#app/data/battle-stat"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { Status, StatusEffect } from "#app/data/status-effect"; import Phaser from "phaser";
import { GameModes, getGameMode } from "#app/game-mode"; import GameManager from "#test/utils/gameManager";
import { EncounterPhase } from "#app/phases/encounter-phase";
import { SelectStarterPhase } from "#app/phases/select-starter-phase";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import { Stat } from "#enums/stat";
import { getMovePosition } from "#test/utils/gameManagerUtils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import { generateStarter } from "#test/utils/gameManagerUtils";
import { SPLASH_ONLY } from "#test/utils/testUtils"; import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Intimidate", () => { describe("Abilities - Intimidate", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -29,257 +25,113 @@ describe("Abilities - Intimidate", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override.battleType("single")
.battleType("single") .enemySpecies(Species.RATTATA)
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.INTIMIDATE) .enemyAbility(Abilities.INTIMIDATE)
.enemyPassiveAbility(Abilities.HYDRATION)
.ability(Abilities.INTIMIDATE) .ability(Abilities.INTIMIDATE)
.moveset([Moves.SPLASH, Moves.AERIAL_ACE]) .startingWave(3)
.enemyMoveset(SPLASH_ONLY); .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.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]);
game.onNextPrompt(
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; "CheckSwitchPhase",
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); Mode.CONFIRM,
() => {
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; game.setMode(Mode.MESSAGE);
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); game.endPhase();
},
game.doSwitchPokemon(1); () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("TurnInitPhase")
await game.phaseInterceptor.to("CommandPhase"); );
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(0);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2);
}, 20000);
it("single - boss should only trigger once then switch", async () => {
game.override.startingWave(10);
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
game.doSwitchPokemon(1);
await game.phaseInterceptor.to("CommandPhase");
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(0);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2);
}, 20000);
it("single - trainer should only trigger once with switch", async () => {
game.override.startingWave(5);
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
game.doSwitchPokemon(1);
await game.phaseInterceptor.to("CommandPhase");
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(0);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2);
}, 200000);
it("double - trainer should only trigger once per pokemon", async () => {
game.override
.battleType("double")
.startingWave(5);
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2);
const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats;
expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2);
const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2);
const battleStatsPokemon2 = game.scene.getParty()[1].summonData.battleStats;
expect(battleStatsPokemon2[BattleStat.ATK]).toBe(-2);
}, 20000);
it("double - wild: should only trigger once per pokemon", async () => {
game.override.battleType("double");
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2);
const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats;
expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2);
const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2);
const battleStatsPokemon2 = game.scene.getParty()[1].summonData.battleStats;
expect(battleStatsPokemon2[BattleStat.ATK]).toBe(-2);
}, 20000);
it("double - boss: should only trigger once per pokemon", async () => {
game.override
.battleType("double")
.startingWave(10);
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2);
const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats;
expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2);
const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2);
const battleStatsPokemon2 = game.scene.getParty()[1].summonData.battleStats;
expect(battleStatsPokemon2[BattleStat.ATK]).toBe(-2);
}, 20000);
it("single - wild next wave opp triger once, us: none", async () => {
await game.startBattle([Species.MIGHTYENA]);
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
game.move.select(Moves.AERIAL_ACE);
await game.phaseInterceptor.to("DamagePhase");
await game.doKillOpponents();
await game.toNextWave();
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
}, 20000);
it("single - wild next turn - no retrigger on next turn", async () => {
await game.startBattle([Species.MIGHTYENA]);
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
}, 20000);
it("single - trainer should only trigger once and each time he switch", async () => {
game.override
.enemyMoveset(Array(4).fill(Moves.VOLT_SWITCH))
.startingWave(5);
await game.startBattle([Species.MIGHTYENA]);
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-3);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
}, 200000);
it("single - trainer should only trigger once whatever turn we are", async () => {
game.override.startingWave(5);
await game.startBattle([Species.MIGHTYENA]);
const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
}, 20000);
it("double - wild vs only 1 on player side", async () => {
game.override.battleType("double");
await game.classicMode.runToSummon([Species.MIGHTYENA]);
await game.phaseInterceptor.to("CommandPhase", false); await game.phaseInterceptor.to("CommandPhase", false);
const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; let playerPokemon = game.scene.getPlayerPokemon()!;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); const enemyPokemon = game.scene.getEnemyPokemon()!;
const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; expect(playerPokemon.species.speciesId).toBe(Species.MIGHTYENA);
expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-1); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1);
const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; game.doSwitchPokemon(1);
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2); await game.phaseInterceptor.run("CommandPhase");
await game.phaseInterceptor.to("CommandPhase");
playerPokemon = game.scene.getPlayerPokemon()!;
expect(playerPokemon.species.speciesId).toBe(Species.POOCHYENA);
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2);
}, 20000); }, 20000);
it("double - wild vs only 1 alive on player side", 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.battleType("double")
await game.runToTitle(); .startingWave(3);
await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]);
game.onNextPrompt("TitlePhase", Mode.TITLE, () => { game.onNextPrompt(
game.scene.gameMode = getGameMode(GameModes.CLASSIC); "CheckSwitchPhase",
const starters = generateStarter(game.scene, [Species.MIGHTYENA, Species.POOCHYENA]); Mode.CONFIRM,
const selectStarterPhase = new SelectStarterPhase(game.scene); () => {
game.scene.pushPhase(new EncounterPhase(game.scene, false)); game.setMode(Mode.MESSAGE);
selectStarterPhase.initBattle(starters); game.endPhase();
game.scene.getParty()[1].hp = 0; },
game.scene.getParty()[1].status = new Status(StatusEffect.FAINT); () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("TurnInitPhase")
}); );
await game.phaseInterceptor.run(EncounterPhase);
await game.phaseInterceptor.to("CommandPhase", false); await game.phaseInterceptor.to("CommandPhase", false);
const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; const playerField = game.scene.getPlayerField()!;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); const enemyField = game.scene.getEnemyField()!;
const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; expect(enemyField[0].getStatStage(Stat.ATK)).toBe(-2);
expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-1); expect(enemyField[1].getStatStage(Stat.ATK)).toBe(-2);
expect(playerField[0].getStatStage(Stat.ATK)).toBe(-2);
const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(playerField[1].getStatStage(Stat.ATK)).toBe(-2);
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2);
}, 20000); }, 20000);
it("should not activate again if there is no switch or new entry", async () => {
game.override.startingWave(2);
game.override.moveset([Moves.SPLASH]);
await game.classicMode.startBattle([ Species.MIGHTYENA, Species.POOCHYENA ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1);
}, 20000);
it("should lower ATK stat stage by 1 for every switch", async () => {
game.override.moveset([Moves.SPLASH])
.enemyMoveset(new Array(4).fill(Moves.VOLT_SWITCH))
.startingWave(5);
await game.classicMode.startBattle([ Species.MIGHTYENA, Species.POOCHYENA ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
let enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1);
game.move.select(getMovePosition(game.scene, 0, Moves.SPLASH));
await game.toNextTurn();
enemyPokemon = game.scene.getEnemyPokemon()!;
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-2);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
enemyPokemon = game.scene.getEnemyPokemon()!;
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-3);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
}, 200000);
}); });

View File

@ -1,8 +1,8 @@
import { BattleStat } from "#app/data/battle-stat"; import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@ -29,14 +29,17 @@ describe("Abilities - Intrepid Sword", () => {
game.override.ability(Abilities.INTREPID_SWORD); game.override.ability(Abilities.INTREPID_SWORD);
}); });
it("INTREPID SWORD on player", async() => { it("should raise ATK stat stage by 1 on entry", async() => {
await game.classicMode.runToSummon([ await game.classicMode.runToSummon([
Species.ZACIAN, Species.ZACIAN,
]); ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
await game.phaseInterceptor.to(CommandPhase, false); await game.phaseInterceptor.to(CommandPhase, false);
const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(1); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1);
const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
expect(battleStatsOpponent[BattleStat.ATK]).toBe(1);
}, 20000); }, 20000);
}); });

View File

@ -1,18 +1,16 @@
import { BattleStat } from "#app/data/battle-stat"; import { BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import { SPLASH_ONLY } from "#test/utils/testUtils"; import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Abilities - Moody", () => { describe("Abilities - Moody", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
let game: GameManager; let game: GameManager;
const battleStatsArray = [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD];
beforeAll(() => { beforeAll(() => {
phaserGame = new Phaser.Game({ phaserGame = new Phaser.Game({
type: Phaser.HEADLESS, type: Phaser.HEADLESS,
@ -30,63 +28,61 @@ describe("Abilities - Moody", () => {
.battleType("single") .battleType("single")
.enemySpecies(Species.RATTATA) .enemySpecies(Species.RATTATA)
.enemyAbility(Abilities.BALL_FETCH) .enemyAbility(Abilities.BALL_FETCH)
.enemyPassiveAbility(Abilities.HYDRATION)
.ability(Abilities.MOODY) .ability(Abilities.MOODY)
.enemyMoveset(SPLASH_ONLY) .enemyMoveset(SPLASH_ONLY)
.moveset(SPLASH_ONLY); .moveset(SPLASH_ONLY);
}); });
it( it("should increase one stat stage by 2 and decrease a different stat stage by 1",
"should increase one BattleStat by 2 stages and decrease a different BattleStat by 1 stage",
async () => { async () => {
await game.startBattle(); await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.toNextTurn(); await game.toNextTurn();
// Find the increased and decreased stats, make sure they are different. // Find the increased and decreased stats, make sure they are different.
const statChanges = playerPokemon.summonData.battleStats; const changedStats = EFFECTIVE_STATS.filter(s => playerPokemon.getStatStage(s) === 2 || playerPokemon.getStatStage(s) === -1);
const changedStats = battleStatsArray.filter(bs => statChanges[bs] === 2 || statChanges[bs] === -1);
expect(changedStats).toBeTruthy(); expect(changedStats).toBeTruthy();
expect(changedStats.length).toBe(2); expect(changedStats.length).toBe(2);
expect(changedStats[0] !== changedStats[1]).toBeTruthy(); expect(changedStats[0] !== changedStats[1]).toBeTruthy();
}); });
it( it("should only increase one stat stage by 2 if all stat stages are at -6",
"should only increase one BattleStat by 2 stages if all BattleStats are at -6",
async () => { async () => {
await game.startBattle(); await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;
// Set all BattleStats to -6
battleStatsArray.forEach(bs => playerPokemon.summonData.battleStats[bs] = -6); // Set all stat stages to -6
vi.spyOn(playerPokemon.summonData, "statStages", "get").mockReturnValue(new Array(BATTLE_STATS.length).fill(-6));
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.toNextTurn(); await game.toNextTurn();
// Should increase one BattleStat by 2 (from -6, meaning it will be -4) // Should increase one stat stage by 2 (from -6, meaning it will be -4)
const increasedStat = battleStatsArray.filter(bs => playerPokemon.summonData.battleStats[bs] === -4); const increasedStat = EFFECTIVE_STATS.filter(s => playerPokemon.getStatStage(s) === -4);
expect(increasedStat).toBeTruthy(); expect(increasedStat).toBeTruthy();
expect(increasedStat.length).toBe(1); expect(increasedStat.length).toBe(1);
}); });
it( it("should only decrease one stat stage by 1 stage if all stat stages are at 6",
"should only decrease one BattleStat by 1 stage if all BattleStats are at 6",
async () => { async () => {
await game.startBattle(); await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;
// Set all BattleStats to 6
battleStatsArray.forEach(bs => playerPokemon.summonData.battleStats[bs] = 6); // Set all stat stages to 6
vi.spyOn(playerPokemon.summonData, "statStages", "get").mockReturnValue(new Array(BATTLE_STATS.length).fill(6));
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.toNextTurn(); await game.toNextTurn();
// Should decrease one BattleStat by 1 (from 6, meaning it will be 5) // Should decrease one stat stage by 1 (from 6, meaning it will be 5)
const decreasedStat = battleStatsArray.filter(bs => playerPokemon.summonData.battleStats[bs] === 5); const decreasedStat = EFFECTIVE_STATS.filter(s => playerPokemon.getStatStage(s) === 5);
expect(decreasedStat).toBeTruthy(); expect(decreasedStat).toBeTruthy();
expect(decreasedStat.length).toBe(1); expect(decreasedStat.length).toBe(1);
}); });

View File

@ -1,14 +1,15 @@
import { BattleStat } from "#app/data/battle-stat"; import { Stat } from "#enums/stat";
import { Stat } from "#app/data/pokemon-stat"; import GameManager from "#test/utils/gameManager";
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
import { VictoryPhase } from "#app/phases/victory-phase";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
import { BattlerIndex } from "#app/battle";
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
import { VictoryPhase } from "#app/phases/victory-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
describe("Abilities - Moxie", () => { describe("Abilities - Moxie", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -32,23 +33,47 @@ describe("Abilities - Moxie", () => {
game.override.enemyAbility(Abilities.MOXIE); game.override.enemyAbility(Abilities.MOXIE);
game.override.ability(Abilities.MOXIE); game.override.ability(Abilities.MOXIE);
game.override.startingLevel(2000); game.override.startingLevel(2000);
game.override.moveset([moveToUse]); game.override.moveset([ moveToUse ]);
game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); game.override.enemyMoveset(SPLASH_ONLY);
}); });
it("MOXIE", async () => { it("should raise ATK stat stage by 1 when winning a battle", async() => {
const moveToUse = Moves.AERIAL_ACE; const moveToUse = Moves.AERIAL_ACE;
await game.startBattle([ await game.startBattle([
Species.MIGHTYENA, Species.MIGHTYENA,
Species.MIGHTYENA, Species.MIGHTYENA,
]); ]);
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; const playerPokemon = game.scene.getPlayerPokemon()!;
expect(battleStatsPokemon[Stat.ATK]).toBe(0);
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0);
game.move.select(moveToUse); game.move.select(moveToUse);
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase); await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase);
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(1); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1);
}, 20000);
// TODO: Activate this test when MOXIE is corrected to work on faint and not on battle victory
it.todo("should raise ATK stat stage by 1 when defeating an ally Pokemon", async() => {
game.override.battleType("double");
const moveToUse = Moves.AERIAL_ACE;
await game.startBattle([
Species.MIGHTYENA,
Species.MIGHTYENA,
]);
const [ firstPokemon, secondPokemon ] = game.scene.getPlayerField();
expect(firstPokemon.getStatStage(Stat.ATK)).toBe(0);
secondPokemon.hp = 1;
game.move.select(moveToUse);
game.selectTarget(BattlerIndex.PLAYER_2);
await game.phaseInterceptor.to(TurnEndPhase);
expect(firstPokemon.getStatStage(Stat.ATK)).toBe(1);
}, 20000); }, 20000);
}); });

View File

@ -1,10 +1,10 @@
import { BattleStat } from "#app/data/battle-stat";
import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { TurnStartPhase } from "#app/phases/turn-start-phase"; import { TurnStartPhase } from "#app/phases/turn-start-phase";
import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Stat } from "#enums/stat";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@ -58,8 +58,9 @@ describe("Abilities - Mycelium Might", () => {
expect(speedOrder).toEqual([playerIndex, enemyIndex]); expect(speedOrder).toEqual([playerIndex, enemyIndex]);
expect(commandOrder).toEqual([enemyIndex, playerIndex]); expect(commandOrder).toEqual([enemyIndex, playerIndex]);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
// Despite the opponent's ability (Clear Body), its attack stat is still reduced.
expect(enemyPokemon?.summonData.battleStats[BattleStat.ATK]).toBe(-1); // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1);
}, 20000); }, 20000);
it("will still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => { it("will still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => {
@ -81,8 +82,8 @@ describe("Abilities - Mycelium Might", () => {
expect(speedOrder).toEqual([playerIndex, enemyIndex]); expect(speedOrder).toEqual([playerIndex, enemyIndex]);
expect(commandOrder).toEqual([playerIndex, enemyIndex]); expect(commandOrder).toEqual([playerIndex, enemyIndex]);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
// Despite the opponent's ability (Clear Body), its attack stat is still reduced. // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
expect(enemyPokemon?.summonData.battleStats[BattleStat.ATK]).toBe(-1); expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1);
}, 20000); }, 20000);
it("will not affect non-status moves", async () => { it("will not affect non-status moves", async () => {

View File

@ -1,4 +1,4 @@
import { BattleStat } from "#app/data/battle-stat"; import { Stat } from "#enums/stat";
import { StatusEffect } from "#app/data/status-effect"; import { StatusEffect } from "#app/data/status-effect";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import { BattlerTagType } from "#app/enums/battler-tag-type"; import { BattlerTagType } from "#app/enums/battler-tag-type";
@ -96,7 +96,7 @@ describe("Abilities - Parental Bond", () => {
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to(BerryPhase, false);
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(2); expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2);
}, TIMEOUT }, TIMEOUT
); );
@ -116,7 +116,7 @@ describe("Abilities - Parental Bond", () => {
game.move.select(Moves.BABY_DOLL_EYES); game.move.select(Moves.BABY_DOLL_EYES);
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to(BerryPhase, false);
expect(enemyPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-1); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
}, TIMEOUT }, TIMEOUT
); );
@ -568,7 +568,7 @@ describe("Abilities - Parental Bond", () => {
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to(BerryPhase, false);
expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-1); expect(leadPokemon.getStatStage(Stat.ATK)).toBe(-1);
}, TIMEOUT }, TIMEOUT
); );
@ -590,7 +590,7 @@ describe("Abilities - Parental Bond", () => {
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to(BerryPhase, false);
expect(enemyPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(1); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(1);
}, TIMEOUT }, TIMEOUT
); );

View File

@ -1,5 +1,5 @@
import { BattleStatMultiplierAbAttr, allAbilities } from "#app/data/ability"; import { StatMultiplierAbAttr, allAbilities } from "#app/data/ability";
import { BattleStat } from "#app/data/battle-stat"; import { Stat } from "#enums/stat";
import { WeatherType } from "#app/data/weather"; import { WeatherType } from "#app/data/weather";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
@ -49,10 +49,10 @@ describe("Abilities - Sand Veil", () => {
vi.spyOn(leadPokemon[0], "getAbility").mockReturnValue(allAbilities[Abilities.SAND_VEIL]); vi.spyOn(leadPokemon[0], "getAbility").mockReturnValue(allAbilities[Abilities.SAND_VEIL]);
const sandVeilAttr = allAbilities[Abilities.SAND_VEIL].getAttrs(BattleStatMultiplierAbAttr)[0]; const sandVeilAttr = allAbilities[Abilities.SAND_VEIL].getAttrs(StatMultiplierAbAttr)[0];
vi.spyOn(sandVeilAttr, "applyBattleStat").mockImplementation( vi.spyOn(sandVeilAttr, "applyStatStage").mockImplementation(
(pokemon, passive, simulated, battleStat, statValue, args) => { (_pokemon, _passive, _simulated, stat, statValue, _args) => {
if (battleStat === BattleStat.EVA && game.scene.arena.weather?.weatherType === WeatherType.SANDSTORM) { if (stat === Stat.EVA && game.scene.arena.weather?.weatherType === WeatherType.SANDSTORM) {
statValue.value *= -1; // will make all attacks miss statValue.value *= -1; // will make all attacks miss
return true; return true;
} }

View File

@ -1,4 +1,4 @@
import { BattleStat } from "#app/data/battle-stat"; import { Stat } from "#enums/stat";
import { TerrainType } from "#app/data/terrain"; import { TerrainType } from "#app/data/terrain";
import { MoveEndPhase } from "#app/phases/move-end-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase";
@ -9,6 +9,7 @@ import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
// See also: TypeImmunityAbAttr // See also: TypeImmunityAbAttr
describe("Abilities - Sap Sipper", () => { describe("Abilities - Sap Sipper", () => {
@ -31,52 +32,55 @@ describe("Abilities - Sap Sipper", () => {
game.override.disableCrits(); 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 moveToUse = Moves.LEAFAGE;
const enemyAbility = Abilities.SAP_SIPPER; const enemyAbility = Abilities.SAP_SIPPER;
game.override.moveset([moveToUse]); game.override.moveset([ moveToUse ]);
game.override.enemyMoveset([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]); game.override.enemyMoveset(SPLASH_ONLY);
game.override.enemySpecies(Species.DUSKULL); game.override.enemySpecies(Species.DUSKULL);
game.override.enemyAbility(enemyAbility); game.override.enemyAbility(enemyAbility);
await game.startBattle(); await game.startBattle();
const startingOppHp = game.scene.currentBattle.enemyParty[0].hp; const enemyPokemon = game.scene.getEnemyPokemon()!;
const initialEnemyHp = enemyPokemon.hp;
game.move.select(moveToUse); game.move.select(moveToUse);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(startingOppHp - game.scene.getEnemyParty()[0].hp).toBe(0); expect(initialEnemyHp - enemyPokemon.hp).toBe(0);
expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(1); 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 moveToUse = Moves.SPORE;
const enemyAbility = Abilities.SAP_SIPPER; const enemyAbility = Abilities.SAP_SIPPER;
game.override.moveset([moveToUse]); game.override.moveset([ moveToUse ]);
game.override.enemyMoveset([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]); game.override.enemyMoveset(SPLASH_ONLY);
game.override.enemySpecies(Species.RATTATA); game.override.enemySpecies(Species.RATTATA);
game.override.enemyAbility(enemyAbility); game.override.enemyAbility(enemyAbility);
await game.startBattle(); await game.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(moveToUse); game.move.select(moveToUse);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(game.scene.getEnemyParty()[0].status).toBeUndefined(); expect(enemyPokemon.status).toBeUndefined();
expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(1); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
}); });
it("do not activate against status moves that target the field", async () => { it("do not activate against status moves that target the field", async () => {
const moveToUse = Moves.GRASSY_TERRAIN; const moveToUse = Moves.GRASSY_TERRAIN;
const enemyAbility = Abilities.SAP_SIPPER; const enemyAbility = Abilities.SAP_SIPPER;
game.override.moveset([moveToUse]); game.override.moveset([ moveToUse ]);
game.override.enemyMoveset([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]); game.override.enemyMoveset(SPLASH_ONLY);
game.override.enemySpecies(Species.RATTATA); game.override.enemySpecies(Species.RATTATA);
game.override.enemyAbility(enemyAbility); game.override.enemyAbility(enemyAbility);
@ -88,51 +92,54 @@ describe("Abilities - Sap Sipper", () => {
expect(game.scene.arena.terrain).toBeDefined(); expect(game.scene.arena.terrain).toBeDefined();
expect(game.scene.arena.terrain!.terrainType).toBe(TerrainType.GRASSY); 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 () => { it("activate once against multi-hit grass attacks", async () => {
const moveToUse = Moves.BULLET_SEED; const moveToUse = Moves.BULLET_SEED;
const enemyAbility = Abilities.SAP_SIPPER; const enemyAbility = Abilities.SAP_SIPPER;
game.override.moveset([moveToUse]); game.override.moveset([ moveToUse ]);
game.override.enemyMoveset([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]); game.override.enemyMoveset(SPLASH_ONLY);
game.override.enemySpecies(Species.RATTATA); game.override.enemySpecies(Species.RATTATA);
game.override.enemyAbility(enemyAbility); game.override.enemyAbility(enemyAbility);
await game.startBattle(); await game.startBattle();
const startingOppHp = game.scene.currentBattle.enemyParty[0].hp; const enemyPokemon = game.scene.getEnemyPokemon()!;
const initialEnemyHp = enemyPokemon.hp;
game.move.select(moveToUse); game.move.select(moveToUse);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(startingOppHp - game.scene.getEnemyParty()[0].hp).toBe(0); expect(initialEnemyHp - enemyPokemon.hp).toBe(0);
expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(1); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
}); });
it("do not activate against status moves that target the user", async () => { it("do not activate against status moves that target the user", async () => {
const moveToUse = Moves.SPIKY_SHIELD; const moveToUse = Moves.SPIKY_SHIELD;
const ability = Abilities.SAP_SIPPER; const ability = Abilities.SAP_SIPPER;
game.override.moveset([moveToUse]); game.override.moveset([ moveToUse ]);
game.override.ability(ability); 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.enemySpecies(Species.RATTATA);
game.override.enemyAbility(Abilities.NONE); game.override.enemyAbility(Abilities.NONE);
await game.startBattle(); await game.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(moveToUse); game.move.select(moveToUse);
await game.phaseInterceptor.to(MoveEndPhase); 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); 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"); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
}); });
@ -149,13 +156,14 @@ describe("Abilities - Sap Sipper", () => {
await game.startBattle(); await game.startBattle();
const startingOppHp = game.scene.currentBattle.enemyParty[0].hp; const enemyPokemon = game.scene.getEnemyPokemon()!;
const initialEnemyHp = enemyPokemon.hp;
game.move.select(moveToUse); game.move.select(moveToUse);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(startingOppHp - game.scene.getEnemyParty()[0].hp).toBe(0); expect(initialEnemyHp - enemyPokemon.hp).toBe(0);
expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(1); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
}); });
}); });

View File

@ -1,6 +1,6 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
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 { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";

View File

@ -1,6 +1,6 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
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 { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";

View File

@ -1,6 +1,6 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
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 { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";

View File

@ -0,0 +1,42 @@
import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
describe("Abilities - Simple", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.enemySpecies(Species.BULBASAUR)
.enemyAbility(Abilities.SIMPLE)
.ability(Abilities.INTIMIDATE)
.enemyMoveset(SPLASH_ONLY);
});
it("should double stat changes when applied", async() => {
await game.startBattle([
Species.SLOWBRO
]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2);
}, 20000);
});

View File

@ -1,4 +1,4 @@
import { BattleStat } from "#app/data/battle-stat"; import { Stat } from "#enums/stat";
import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
@ -41,12 +41,14 @@ describe("Abilities - Volt Absorb", () => {
await game.startBattle(); await game.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(moveToUse); game.move.select(moveToUse);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(game.scene.getParty()[0].summonData.battleStats[BattleStat.SPDEF]).toBe(1); expect(playerPokemon.getStatStage(Stat.SPDEF)).toBe(1);
expect(game.scene.getParty()[0].getTag(BattlerTagType.CHARGED)).toBeDefined(); expect(playerPokemon.getTag(BattlerTagType.CHARGED)).toBeDefined();
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
}); });
}); });

View File

@ -1,8 +1,8 @@
import { BattleStat } from "#app/data/battle-stat"; import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import { SPLASH_ONLY } from "#test/utils/testUtils"; import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@ -31,56 +31,38 @@ describe("Abilities - Wind Rider", () => {
.enemyMoveset(SPLASH_ONLY); .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.classicMode.startBattle([Species.MAGIKARP]); await game.classicMode.startBattle([ Species.MAGIKARP ]);
const shiftry = game.scene.getEnemyPokemon()!; const shiftry = game.scene.getEnemyPokemon()!;
expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0); expect(shiftry.getStatStage(Stat.ATK)).toBe(0);
game.move.select(Moves.PETAL_BLIZZARD); game.move.select(Moves.PETAL_BLIZZARD);
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
expect(shiftry.isFullHp()).toBe(true); 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
game.override.enemySpecies(Species.MAGIKARP); .enemySpecies(Species.MAGIKARP)
.ability(Abilities.WIND_RIDER);
await game.classicMode.startBattle([Species.SHIFTRY]); await game.classicMode.startBattle([Species.SHIFTRY]);
const shiftry = game.scene.getPlayerPokemon()!; const shiftry = game.scene.getPlayerPokemon()!;
expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0); expect(shiftry.getStatStage(Stat.ATK)).toBe(0);
game.move.select(Moves.TAILWIND); game.move.select(Moves.TAILWIND);
await game.phaseInterceptor.to("TurnEndPhase"); 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);
await game.classicMode.startBattle([Species.SHIFTRY]);
const magikarp = game.scene.getEnemyPokemon()!;
const shiftry = game.scene.getPlayerPokemon()!;
expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0);
expect(magikarp.summonData.battleStats[BattleStat.ATK]).toBe(0);
game.move.select(Moves.TAILWIND);
await game.phaseInterceptor.to("TurnEndPhase");
expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(1);
expect(magikarp.summonData.battleStats[BattleStat.ATK]).toBe(0);
});
it("does not increase Attack when Tailwind is present on opposing side", async () => {
game.override game.override
.enemySpecies(Species.MAGIKARP) .enemySpecies(Species.MAGIKARP)
.ability(Abilities.WIND_RIDER); .ability(Abilities.WIND_RIDER);
@ -89,15 +71,35 @@ describe("Abilities - Wind Rider", () => {
const magikarp = game.scene.getEnemyPokemon()!; const magikarp = game.scene.getEnemyPokemon()!;
const shiftry = game.scene.getPlayerPokemon()!; const shiftry = game.scene.getPlayerPokemon()!;
expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0); expect(shiftry.getStatStage(Stat.ATK)).toBe(0);
expect(magikarp.summonData.battleStats[BattleStat.ATK]).toBe(0); expect(magikarp.getStatStage(Stat.ATK)).toBe(0);
game.move.select(Moves.TAILWIND); game.move.select(Moves.TAILWIND);
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(1); expect(shiftry.getStatStage(Stat.ATK)).toBe(1);
expect(magikarp.summonData.battleStats[BattleStat.ATK]).toBe(0); expect(magikarp.getStatStage(Stat.ATK)).toBe(0);
});
it("does not raise ATK stat stage when Tailwind is present on opposing side", async () => {
game.override
.enemySpecies(Species.MAGIKARP)
.ability(Abilities.WIND_RIDER);
await game.classicMode.startBattle([Species.SHIFTRY]);
const magikarp = game.scene.getEnemyPokemon()!;
const shiftry = game.scene.getPlayerPokemon()!;
expect(shiftry.getStatStage(Stat.ATK)).toBe(0);
expect(magikarp.getStatStage(Stat.ATK)).toBe(0);
game.move.select(Moves.TAILWIND);
await game.phaseInterceptor.to("TurnEndPhase");
expect(shiftry.getStatStage(Stat.ATK)).toBe(1);
expect(magikarp.getStatStage(Stat.ATK)).toBe(0);
}); });
it("does not interact with Sandstorm", async () => { it("does not interact with Sandstorm", async () => {
@ -106,14 +108,14 @@ describe("Abilities - Wind Rider", () => {
await game.classicMode.startBattle([Species.SHIFTRY]); await game.classicMode.startBattle([Species.SHIFTRY]);
const shiftry = game.scene.getPlayerPokemon()!; 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); expect(shiftry.isFullHp()).toBe(true);
game.move.select(Moves.SANDSTORM); game.move.select(Moves.SANDSTORM);
await game.phaseInterceptor.to("TurnEndPhase"); 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()); expect(shiftry.hp).lessThan(shiftry.getMaxHp());
}); });
}); });

View File

@ -1,6 +1,5 @@
import { Stat } from "#enums/stat";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { Stat } from "#app/data/pokemon-stat";
import { Status, StatusEffect } from "#app/data/status-effect";
import { DamagePhase } from "#app/phases/damage-phase"; import { DamagePhase } from "#app/phases/damage-phase";
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
import { MessagePhase } from "#app/phases/message-phase"; import { MessagePhase } from "#app/phases/message-phase";
@ -18,6 +17,7 @@ import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
import { Status, StatusEffect } from "#app/data/status-effect";
const TIMEOUT = 20 * 1000; const TIMEOUT = 20 * 1000;

View File

@ -224,7 +224,7 @@ describe("achvs", () => {
expect(achvs._50_RIBBONS).toBeInstanceOf(RibbonAchv); expect(achvs._50_RIBBONS).toBeInstanceOf(RibbonAchv);
expect(achvs._75_RIBBONS).toBeInstanceOf(RibbonAchv); expect(achvs._75_RIBBONS).toBeInstanceOf(RibbonAchv);
expect(achvs._100_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.MAX_FRIENDSHIP).toBeInstanceOf(Achv);
expect(achvs.MEGA_EVOLVE).toBeInstanceOf(Achv); expect(achvs.MEGA_EVOLVE).toBeInstanceOf(Achv);
expect(achvs.GIGANTAMAX).toBeInstanceOf(Achv); expect(achvs.GIGANTAMAX).toBeInstanceOf(Achv);

View File

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

View File

@ -1,5 +1,5 @@
import { allSpecies } from "#app/data/pokemon-species"; import { allSpecies } from "#app/data/pokemon-species";
import { TempBattleStat } from "#app/data/temp-battle-stat"; import { Stat } from "#enums/stat";
import { GameModes, getGameMode } from "#app/game-mode"; import { GameModes, getGameMode } from "#app/game-mode";
import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
@ -320,7 +320,7 @@ describe("Test Battle Phase", () => {
.startingLevel(100) .startingLevel(100)
.moveset([moveToUse]) .moveset([moveToUse])
.enemyMoveset(SPLASH_ONLY) .enemyMoveset(SPLASH_ONLY)
.startingHeldItems([{ name: "TEMP_STAT_BOOSTER", type: TempBattleStat.ACC }]); .startingHeldItems([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }]);
await game.startBattle(); await game.startBattle();
game.scene.getPlayerPokemon()!.hp = 1; game.scene.getPlayerPokemon()!.hp = 1;

View File

@ -1,16 +1,16 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { BattleStat } from "#app/data/battle-stat";
import { BattlerTag, BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import Pokemon from "#app/field/pokemon";
import { StatChangePhase } from "#app/phases/stat-change-phase";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import Pokemon from "#app/field/pokemon";
import { BattlerTag, BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Stat } from "#enums/stat";
vi.mock("#app/battle-scene.js"); 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 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)["levels"]).toEqual(-1); expect((phase as StatStageChangePhase)["stages"]).toEqual(-1);
expect((phase as StatChangePhase)["stats"]).toEqual([BattleStat.DEF, BattleStat.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

@ -1,10 +1,10 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { BattleStat } from "#app/data/battle-stat";
import { StockpilingTag } from "#app/data/battler-tags";
import Pokemon, { PokemonSummonData } from "#app/field/pokemon";
import * as messages from "#app/messages";
import { StatChangePhase } from "#app/phases/stat-change-phase";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import Pokemon, { PokemonSummonData } from "#app/field/pokemon";
import { StockpilingTag } from "#app/data/battler-tags";
import { Stat } from "#enums/stat";
import * as messages from "#app/messages";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
beforeEach(() => { beforeEach(() => {
vi.spyOn(messages, "getPokemonNameWithAffix").mockImplementation(() => ""); vi.spyOn(messages, "getPokemonNameWithAffix").mockImplementation(() => "");
@ -12,7 +12,7 @@ beforeEach(() => {
describe("BattlerTag - StockpilingTag", () => { describe("BattlerTag - StockpilingTag", () => {
describe("onAdd", () => { describe("onAdd", () => {
it("unshifts a StatChangePhase with expected stat changes on add", { timeout: 10000 }, async () => { it("unshifts a StatStageChangePhase with expected stat stage changes on add", { timeout: 10000 }, async () => {
const mockPokemon = { 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)["levels"]).toEqual(1); expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF])); expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ]));
(phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.DEF, BattleStat.SPDEF], [1, 1]); (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [Stat.DEF, Stat.SPDEF], [1, 1]);
}); });
subject.onAdd(mockPokemon); 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.summonData.battleStats[BattleStat.DEF] = 6; mockPokemon.summonData.statStages[Stat.DEF - 1] = 6;
mockPokemon.summonData.battleStats[BattleStat.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)["levels"]).toEqual(1); expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF])); expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([Stat.DEF, Stat.SPDEF]));
(phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.DEF, BattleStat.SPDEF], [1, 1]); (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [ Stat.DEF, Stat.SPDEF ], [1, 1]);
}); });
subject.onAdd(mockPokemon); 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)["levels"]).toEqual(1); expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF])); expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([ Stat.DEF, Stat.SPDEF ]));
(phase as StatChangePhase)["onChange"]!(mockPokemon, [BattleStat.DEF, BattleStat.SPDEF], [1, 1]); (phase as StatStageChangePhase)["onChange"]!(mockPokemon, [ Stat.DEF, Stat.SPDEF ], [1, 1]);
}); });
subject.onOverlap(mockPokemon); subject.onOverlap(mockPokemon);
@ -98,39 +98,39 @@ describe("BattlerTag - StockpilingTag", () => {
vi.spyOn(mockPokemon.scene, "queueMessage").mockImplementation(() => {}); vi.spyOn(mockPokemon.scene, "queueMessage").mockImplementation(() => {});
mockPokemon.summonData.battleStats[BattleStat.DEF] = 5; mockPokemon.summonData.statStages[Stat.DEF - 1] = 5;
mockPokemon.summonData.battleStats[BattleStat.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)["levels"]).toEqual(1); expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.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, [BattleStat.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)["levels"]).toEqual(1); expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.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, [BattleStat.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)["levels"]).toEqual(1); expect((phase as StatStageChangePhase)["stages"]).toEqual(1);
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.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
}); });
@ -138,20 +138,20 @@ describe("BattlerTag - StockpilingTag", () => {
subject.onOverlap(mockPokemon); subject.onOverlap(mockPokemon);
expect(subject.stockpiledCount).toBe(3); 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"); throw new Error("Should not be called a fourth time");
}); });
// fourth stack should not be applied // fourth stack should not be applied
subject.onOverlap(mockPokemon); subject.onOverlap(mockPokemon);
expect(subject.stockpiledCount).toBe(3); 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 // 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)["levels"]).toEqual(-2); expect((phase as StatStageChangePhase)["stages"]).toEqual(-2);
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.SPDEF])); expect((phase as StatStageChangePhase)["stats"]).toEqual(expect.arrayContaining([Stat.SPDEF]));
}); });
subject.onRemove(mockPokemon); subject.onRemove(mockPokemon);

View File

@ -5,7 +5,7 @@ import { getPokemonSpecies } from "#app/data/pokemon-species";
import { SPLASH_ONLY } from "./utils/testUtils"; import { SPLASH_ONLY } from "./utils/testUtils";
import { Abilities } from "#app/enums/abilities"; import { Abilities } from "#app/enums/abilities";
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
import { BattleStat } from "#app/data/battle-stat"; import { EFFECTIVE_STATS } from "#app/enums/stat";
import { EnemyPokemon } from "#app/field/pokemon"; import { EnemyPokemon } from "#app/field/pokemon";
import { toDmgValue } from "#app/utils"; import { toDmgValue } from "#app/utils";
@ -80,7 +80,7 @@ describe("Boss Pokemon / Shields", () => {
expect(boss2.bossSegments).toBe(2); expect(boss2.bossSegments).toBe(2);
}, TIMEOUT); }, TIMEOUT);
it("shields should stop overflow damage and give stat boosts when broken", async () => { it("shields should stop overflow damage and give stat stage boosts when broken", async () => {
game.override.startingWave(150); // Floor 150 > 2 shields / 3 health segments game.override.startingWave(150); // Floor 150 > 2 shields / 3 health segments
await game.classicMode.startBattle([ Species.MEWTWO ]); await game.classicMode.startBattle([ Species.MEWTWO ]);
@ -89,7 +89,7 @@ describe("Boss Pokemon / Shields", () => {
const segmentHp = enemyPokemon.getMaxHp() / enemyPokemon.bossSegments; const segmentHp = enemyPokemon.getMaxHp() / enemyPokemon.bossSegments;
expect(enemyPokemon.isBoss()).toBe(true); expect(enemyPokemon.isBoss()).toBe(true);
expect(enemyPokemon.bossSegments).toBe(3); expect(enemyPokemon.bossSegments).toBe(3);
expect(getTotalStatBoosts(enemyPokemon)).toBe(0); expect(getTotalStatStageBoosts(enemyPokemon)).toBe(0);
game.move.select(Moves.SUPER_FANG); // Enough to break the first shield game.move.select(Moves.SUPER_FANG); // Enough to break the first shield
await game.toNextTurn(); await game.toNextTurn();
@ -98,7 +98,7 @@ describe("Boss Pokemon / Shields", () => {
expect(enemyPokemon.bossSegmentIndex).toBe(1); expect(enemyPokemon.bossSegmentIndex).toBe(1);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - toDmgValue(segmentHp)); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - toDmgValue(segmentHp));
// Breaking the shield gives a +1 boost to ATK, DEF, SP ATK, SP DEF or SPD // Breaking the shield gives a +1 boost to ATK, DEF, SP ATK, SP DEF or SPD
expect(getTotalStatBoosts(enemyPokemon)).toBe(1); expect(getTotalStatStageBoosts(enemyPokemon)).toBe(1);
game.move.select(Moves.FALSE_SWIPE); // Enough to break last shield but not kill game.move.select(Moves.FALSE_SWIPE); // Enough to break last shield but not kill
await game.toNextTurn(); await game.toNextTurn();
@ -106,7 +106,7 @@ describe("Boss Pokemon / Shields", () => {
expect(enemyPokemon.bossSegmentIndex).toBe(0); expect(enemyPokemon.bossSegmentIndex).toBe(0);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - toDmgValue(2 * segmentHp)); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - toDmgValue(2 * segmentHp));
// Breaking the last shield gives a +2 boost to ATK, DEF, SP ATK, SP DEF or SPD // Breaking the last shield gives a +2 boost to ATK, DEF, SP ATK, SP DEF or SPD
expect(getTotalStatBoosts(enemyPokemon)).toBe(3); expect(getTotalStatStageBoosts(enemyPokemon)).toBe(3);
}, TIMEOUT); }, TIMEOUT);
@ -146,7 +146,7 @@ describe("Boss Pokemon / Shields", () => {
}, TIMEOUT); }, TIMEOUT);
it("the number of stats boosts is consistent when several shields are broken at once", async () => { it("the number of stat stage boosts is consistent when several shields are broken at once", async () => {
const shieldsToBreak = 4; const shieldsToBreak = 4;
game.override game.override
@ -161,22 +161,22 @@ describe("Boss Pokemon / Shields", () => {
expect(boss1.isBoss()).toBe(true); expect(boss1.isBoss()).toBe(true);
expect(boss1.bossSegments).toBe(shieldsToBreak + 1); expect(boss1.bossSegments).toBe(shieldsToBreak + 1);
expect(boss1.bossSegmentIndex).toBe(shieldsToBreak); expect(boss1.bossSegmentIndex).toBe(shieldsToBreak);
expect(getTotalStatBoosts(boss1)).toBe(0); expect(getTotalStatStageBoosts(boss1)).toBe(0);
let totalStats = 0; let totalStatStages = 0;
// Break the shields one by one // Break the shields one by one
for (let i = 1; i <= shieldsToBreak; i++) { for (let i = 1; i <= shieldsToBreak; i++) {
boss1.damageAndUpdate(singleShieldDamage); boss1.damageAndUpdate(singleShieldDamage);
expect(boss1.bossSegmentIndex).toBe(shieldsToBreak - i); expect(boss1.bossSegmentIndex).toBe(shieldsToBreak - i);
expect(boss1.hp).toBe(boss1.getMaxHp() - toDmgValue(boss1SegmentHp * i)); expect(boss1.hp).toBe(boss1.getMaxHp() - toDmgValue(boss1SegmentHp * i));
// Do nothing and go to next turn so that the StatChangePhase gets applied // Do nothing and go to next turn so that the StatStageChangePhase gets applied
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.toNextTurn(); await game.toNextTurn();
// All broken shields give +1 stat boost, except the last two that gives +2 // All broken shields give +1 stat boost, except the last two that gives +2
totalStats += i >= shieldsToBreak -1? 2 : 1; totalStatStages += i >= shieldsToBreak -1? 2 : 1;
expect(getTotalStatBoosts(boss1)).toBe(totalStats); expect(getTotalStatStageBoosts(boss1)).toBe(totalStatStages);
} }
const boss2: EnemyPokemon = game.scene.getEnemyParty()[1]!; const boss2: EnemyPokemon = game.scene.getEnemyParty()[1]!;
@ -186,35 +186,30 @@ describe("Boss Pokemon / Shields", () => {
expect(boss2.isBoss()).toBe(true); expect(boss2.isBoss()).toBe(true);
expect(boss2.bossSegments).toBe(shieldsToBreak + 1); expect(boss2.bossSegments).toBe(shieldsToBreak + 1);
expect(boss2.bossSegmentIndex).toBe(shieldsToBreak); expect(boss2.bossSegmentIndex).toBe(shieldsToBreak);
expect(getTotalStatBoosts(boss2)).toBe(0); expect(getTotalStatStageBoosts(boss2)).toBe(0);
// Enough damage to break all shields at once // Enough damage to break all shields at once
boss2.damageAndUpdate(Math.ceil(requiredDamage)); boss2.damageAndUpdate(Math.ceil(requiredDamage));
expect(boss2.bossSegmentIndex).toBe(0); expect(boss2.bossSegmentIndex).toBe(0);
expect(boss2.hp).toBe(boss2.getMaxHp() - toDmgValue(boss2SegmentHp * shieldsToBreak)); expect(boss2.hp).toBe(boss2.getMaxHp() - toDmgValue(boss2SegmentHp * shieldsToBreak));
// Do nothing and go to next turn so that the StatChangePhase gets applied // Do nothing and go to next turn so that the StatStageChangePhase gets applied
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.toNextTurn(); await game.toNextTurn();
expect(getTotalStatBoosts(boss2)).toBe(totalStats); expect(getTotalStatStageBoosts(boss2)).toBe(totalStatStages);
}, TIMEOUT); }, TIMEOUT);
/** /**
* Gets the sum of the ATK, DEF, SP ATK, SP DEF and SPD boosts for the given Pokemon * Gets the sum of the effective stat stage boosts for the given Pokemon
* @param enemyPokemon the pokemon to get stats from * @param enemyPokemon the pokemon to get stats from
* @returns the total stats boosts * @returns the total stats boosts
*/ */
function getTotalStatBoosts(enemyPokemon: EnemyPokemon): number { function getTotalStatStageBoosts(enemyPokemon: EnemyPokemon): number {
const enemyBattleStats = enemyPokemon.summonData.battleStats; let boosts = 0;
return enemyBattleStats?.reduce(statsSum, 0); for (const s of EFFECTIVE_STATS) {
} boosts += enemyPokemon.getStatStage(s);
function statsSum(total: number, value: number, index: number) {
if (index <= BattleStat.SPD) {
return total + value;
} }
return total; return boosts;
} }
}); });

View File

@ -0,0 +1,97 @@
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phase from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { TempCritBoosterModifier } from "#app/modifier/modifier";
import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { Button } from "#app/enums/buttons";
import { CommandPhase } from "#app/phases/command-phase";
import { NewBattlePhase } from "#app/phases/new-battle-phase";
import { TurnInitPhase } from "#app/phases/turn-init-phase";
describe("Items - Dire Hit", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phase.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.enemySpecies(Species.MAGIKARP)
.enemyMoveset(SPLASH_ONLY)
.moveset([ Moves.POUND ])
.startingHeldItems([{ name: "DIRE_HIT" }])
.battleType("single")
.disableCrits();
}, 20000);
it("should raise CRIT stage by 1", async () => {
await game.startBattle([
Species.GASTLY
]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "getCritStage");
game.move.select(Moves.POUND);
await game.phaseInterceptor.to(TurnEndPhase);
expect(enemyPokemon.getCritStage).toHaveReturnedWith(1);
}, 20000);
it("should renew how many battles are left of existing DIRE_HIT when picking up new DIRE_HIT", async() => {
game.override.itemRewards([{ name: "DIRE_HIT" }]);
await game.startBattle([
Species.PIKACHU
]);
game.move.select(Moves.SPLASH);
await game.doKillOpponents();
await game.phaseInterceptor.to(BattleEndPhase);
const modifier = game.scene.findModifier(m => m instanceof TempCritBoosterModifier) as TempCritBoosterModifier;
expect(modifier.getBattlesLeft()).toBe(4);
// Forced DIRE_HIT to spawn in the first slot with override
game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler;
// Traverse to first modifier slot
handler.processInput(Button.LEFT);
handler.processInput(Button.UP);
handler.processInput(Button.ACTION);
}, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(NewBattlePhase), true);
await game.phaseInterceptor.to(TurnInitPhase);
// Making sure only one booster is in the modifier list even after picking up another
let count = 0;
for (const m of game.scene.modifiers) {
if (m instanceof TempCritBoosterModifier) {
count++;
expect((m as TempCritBoosterModifier).getBattlesLeft()).toBe(5);
}
}
expect(count).toBe(1);
}, 20000);
});

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";
@ -37,29 +37,29 @@ describe("Items - Eviolite", () => {
const partyMember = game.scene.getParty()[0]; const partyMember = game.scene.getParty()[0];
// Checking consoe log to make sure Eviolite is applied when getBattleStat (with the appropriate stat) is called // Checking console log to make sure Eviolite is applied when getEffectiveStat (with the appropriate stat) is called
partyMember.getBattleStat(Stat.DEF); partyMember.getEffectiveStat(Stat.DEF);
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), ""); 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 // Printing dummy console messages along the way so subsequent checks don't pass because of the first
console.log(""); console.log("");
partyMember.getBattleStat(Stat.SPDEF); partyMember.getEffectiveStat(Stat.SPDEF);
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), ""); expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), "");
console.log(""); console.log("");
partyMember.getBattleStat(Stat.ATK); partyMember.getEffectiveStat(Stat.ATK);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), ""); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), "");
console.log(""); console.log("");
partyMember.getBattleStat(Stat.SPATK); partyMember.getEffectiveStat(Stat.SPATK);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), ""); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), "");
console.log(""); console.log("");
partyMember.getBattleStat(Stat.SPD); partyMember.getEffectiveStat(Stat.SPD);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), ""); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), "");
}); });

View File

@ -1,7 +1,4 @@
import { BattlerIndex } from "#app/battle"; import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { CritBoosterModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
@ -26,91 +23,64 @@ describe("Items - Leek", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.enemySpecies(Species.MAGIKARP); game.override
game.override.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); .enemySpecies(Species.MAGIKARP)
game.override.disableCrits(); .enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH])
.startingHeldItems([{ name: "LEEK" }])
game.override.battleType("single"); .moveset([ Moves.TACKLE ])
.disableCrits()
.battleType("single");
}); });
it("LEEK activates in battle correctly", async () => { it("should raise CRIT stage by 2 when held by FARFETCHD", async () => {
game.override.startingHeldItems([{ name: "LEEK" }]);
game.override.moveset([Moves.POUND]);
const consoleSpy = vi.spyOn(console, "log");
await game.startBattle([ await game.startBattle([
Species.FARFETCHD Species.FARFETCHD
]); ]);
game.move.select(Moves.POUND); const enemyMember = game.scene.getEnemyPokemon()!;
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); vi.spyOn(enemyMember, "getCritStage");
await game.phaseInterceptor.to(MoveEffectPhase); game.move.select(Moves.TACKLE);
expect(consoleSpy).toHaveBeenCalledWith("Applied", "Leek", ""); await game.phaseInterceptor.to(TurnEndPhase);
expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000); }, 20000);
it("LEEK held by FARFETCHD", async () => { it("should raise CRIT stage by 2 when held by GALAR_FARFETCHD", async () => {
await game.startBattle([
Species.FARFETCHD
]);
const partyMember = game.scene.getPlayerPokemon()!;
// Making sure modifier is not applied without holding item
const critLevel = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
expect(critLevel.value).toBe(0);
// Giving Leek to party member and testing if it applies
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
expect(critLevel.value).toBe(2);
}, 20000);
it("LEEK held by GALAR_FARFETCHD", async () => {
await game.startBattle([ await game.startBattle([
Species.GALAR_FARFETCHD Species.GALAR_FARFETCHD
]); ]);
const partyMember = game.scene.getPlayerPokemon()!; const enemyMember = game.scene.getEnemyPokemon()!;
// Making sure modifier is not applied without holding item vi.spyOn(enemyMember, "getCritStage");
const critLevel = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
expect(critLevel.value).toBe(0); game.move.select(Moves.TACKLE);
// Giving Leek to party member and testing if it applies await game.phaseInterceptor.to(TurnEndPhase);
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
expect(critLevel.value).toBe(2); expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000); }, 20000);
it("LEEK held by SIRFETCHD", async () => { it("should raise CRIT stage by 2 when held by SIRFETCHD", async () => {
await game.startBattle([ await game.startBattle([
Species.SIRFETCHD Species.SIRFETCHD
]); ]);
const partyMember = game.scene.getPlayerPokemon()!; const enemyMember = game.scene.getEnemyPokemon()!;
// Making sure modifier is not applied without holding item vi.spyOn(enemyMember, "getCritStage");
const critLevel = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
expect(critLevel.value).toBe(0); game.move.select(Moves.TACKLE);
// Giving Leek to party member and testing if it applies await game.phaseInterceptor.to(TurnEndPhase);
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
expect(critLevel.value).toBe(2); expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000); }, 20000);
it("LEEK held by fused FARFETCHD line (base)", async () => { it("should raise CRIT stage by 2 when held by FARFETCHD line fused with Pokemon", async () => {
// Randomly choose from the Farfetch'd line // Randomly choose from the Farfetch'd line
const species = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.SIRFETCHD]; const species = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.SIRFETCHD];
@ -119,9 +89,7 @@ describe("Items - Leek", () => {
Species.PIKACHU, Species.PIKACHU,
]); ]);
const party = game.scene.getParty(); const [ partyMember, ally ] = game.scene.getParty();
const partyMember = party[0];
const ally = party[1];
// Fuse party members (taken from PlayerPokemon.fuse(...) function) // Fuse party members (taken from PlayerPokemon.fuse(...) function)
partyMember.fusionSpecies = ally.species; partyMember.fusionSpecies = ally.species;
@ -132,20 +100,18 @@ describe("Items - Leek", () => {
partyMember.fusionGender = ally.gender; partyMember.fusionGender = ally.gender;
partyMember.fusionLuck = ally.luck; partyMember.fusionLuck = ally.luck;
// Making sure modifier is not applied without holding item const enemyMember = game.scene.getEnemyPokemon()!;
const critLevel = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
expect(critLevel.value).toBe(0); vi.spyOn(enemyMember, "getCritStage");
// Giving Leek to party member and testing if it applies game.move.select(Moves.TACKLE);
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
expect(critLevel.value).toBe(2); await game.phaseInterceptor.to(TurnEndPhase);
expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000); }, 20000);
it("LEEK held by fused FARFETCHD line (part)", async () => { it("should raise CRIT stage by 2 when held by Pokemon fused with FARFETCHD line", async () => {
// Randomly choose from the Farfetch'd line // Randomly choose from the Farfetch'd line
const species = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.SIRFETCHD]; const species = [Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.SIRFETCHD];
@ -154,9 +120,7 @@ describe("Items - Leek", () => {
species[Utils.randInt(species.length)] species[Utils.randInt(species.length)]
]); ]);
const party = game.scene.getParty(); const [ partyMember, ally ] = game.scene.getParty();
const partyMember = party[0];
const ally = party[1];
// Fuse party members (taken from PlayerPokemon.fuse(...) function) // Fuse party members (taken from PlayerPokemon.fuse(...) function)
partyMember.fusionSpecies = ally.species; partyMember.fusionSpecies = ally.species;
@ -167,36 +131,31 @@ describe("Items - Leek", () => {
partyMember.fusionGender = ally.gender; partyMember.fusionGender = ally.gender;
partyMember.fusionLuck = ally.luck; partyMember.fusionLuck = ally.luck;
// Making sure modifier is not applied without holding item
const critLevel = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
expect(critLevel.value).toBe(0); const enemyMember = game.scene.getEnemyPokemon()!;
// Giving Leek to party member and testing if it applies vi.spyOn(enemyMember, "getCritStage");
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
expect(critLevel.value).toBe(2); game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to(TurnEndPhase);
expect(enemyMember.getCritStage).toHaveReturnedWith(2);
}, 20000); }, 20000);
it("LEEK not held by FARFETCHD line", async () => { it("should not raise CRIT stage when held by a Pokemon outside of FARFETCHD line", async () => {
await game.startBattle([ await game.startBattle([
Species.PIKACHU Species.PIKACHU
]); ]);
const partyMember = game.scene.getPlayerPokemon()!; const enemyMember = game.scene.getEnemyPokemon()!;
// Making sure modifier is not applied without holding item vi.spyOn(enemyMember, "getCritStage");
const critLevel = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
expect(critLevel.value).toBe(0); game.move.select(Moves.TACKLE);
// Giving Leek to party member and testing if it applies await game.phaseInterceptor.to(TurnEndPhase);
partyMember.scene.addModifier(modifierTypes.LEEK().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
expect(critLevel.value).toBe(0); expect(enemyMember.getCritStage).toHaveReturnedWith(0);
}, 20000); }, 20000);
}); });

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";
@ -37,29 +37,29 @@ describe("Items - Light Ball", () => {
const partyMember = game.scene.getParty()[0]; const partyMember = game.scene.getParty()[0];
// Checking consoe log to make sure Light Ball is applied when getBattleStat (with the appropriate stat) is called // Checking console log to make sure Light Ball is applied when getEffectiveStat (with the appropriate stat) is called
partyMember.getBattleStat(Stat.DEF); partyMember.getEffectiveStat(Stat.DEF);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), ""); 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 // Printing dummy console messages along the way so subsequent checks don't pass because of the first
console.log(""); console.log("");
partyMember.getBattleStat(Stat.SPDEF); partyMember.getEffectiveStat(Stat.SPDEF);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), ""); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), "");
console.log(""); console.log("");
partyMember.getBattleStat(Stat.ATK); partyMember.getEffectiveStat(Stat.ATK);
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), ""); expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), "");
console.log(""); console.log("");
partyMember.getBattleStat(Stat.SPATK); partyMember.getEffectiveStat(Stat.SPATK);
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), ""); expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), "");
console.log(""); console.log("");
partyMember.getBattleStat(Stat.SPD); partyMember.getEffectiveStat(Stat.SPD);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), ""); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), "");
}); });

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";
@ -37,29 +37,29 @@ describe("Items - Metal Powder", () => {
const partyMember = game.scene.getParty()[0]; const partyMember = game.scene.getParty()[0];
// Checking consoe log to make sure Metal Powder is applied when getBattleStat (with the appropriate stat) is called // Checking console log to make sure Metal Powder is applied when getEffectiveStat (with the appropriate stat) is called
partyMember.getBattleStat(Stat.DEF); partyMember.getEffectiveStat(Stat.DEF);
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), ""); 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 // Printing dummy console messages along the way so subsequent checks don't pass because of the first
console.log(""); console.log("");
partyMember.getBattleStat(Stat.SPDEF); partyMember.getEffectiveStat(Stat.SPDEF);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), ""); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), "");
console.log(""); console.log("");
partyMember.getBattleStat(Stat.ATK); partyMember.getEffectiveStat(Stat.ATK);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), ""); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), "");
console.log(""); console.log("");
partyMember.getBattleStat(Stat.SPATK); partyMember.getEffectiveStat(Stat.SPATK);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), ""); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), "");
console.log(""); console.log("");
partyMember.getBattleStat(Stat.SPD); partyMember.getEffectiveStat(Stat.SPD);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), ""); expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), "");
}); });

Some files were not shown because too many files have changed in this diff Show More