[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**.
- **Swords Dance**: As a non-attacking move, this move's benefit score is derived entirely from the sum of its attributes' benefit scores. Swords Dance's `StatChangeAttr` has a user benefit score of 0 and a target benefit score that, in this case, simplifies to
- **Swords Dance**: As a non-attacking move, this move's benefit score is derived entirely from the sum of its attributes' benefit scores. Swords Dance's `StatStageChangeAttr` has a user benefit score of 0 and a target benefit score that, in this case, simplifies to
$\text{TBS}=4\times \text{levels} + (-2\times \text{sign(levels)})$
where `levels` is the number of stat stages added by the attribute (in this case, +2). The final score for this move is **6** (Note: because this move is self-targeted, we don't flip the sign of TBS when computing the target score).
- **Crush Claw**: This move is a 75-power Normal-type physical attack with a 50 percent chance to lower the target's Defense by one stage. The additional effect is implemented by the same `StatChangeAttr` as Swords Dance, so we can use the same formulas from before to compute the total TBS and base target score.
- **Crush Claw**: This move is a 75-power Normal-type physical attack with a 50 percent chance to lower the target's Defense by one stage. The additional effect is implemented by the same `StatStageChangeAttr` as Swords Dance, so we can use the same formulas from before to compute the total TBS and base target score.
$\text{TBS}=\text{getTargetBenefitScore(StatChangeAttr)}-\text{attackScore}$
$\text{TBS}=\text{getTargetBenefitScore(StatStageChangeAttr)}-\text{attackScore}$
$\text{TBS}=(-4 + 2)-(-2\times 2 + \lfloor \frac{75}{5} \rfloor)=-2-11=-13$

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

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -89,7 +89,7 @@
"name": "Bänder-Meister",
"name_female": "Bänder-Meisterin"
},
"TRANSFER_MAX_BATTLE_STAT": {
"TRANSFER_MAX_STAT_STAGE": {
"name": "Teamwork",
"description": "Nutze Staffette, während der Anwender mindestens eines Statuswertes maximiert hat."
},

View File

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

View File

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

View File

@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}} nutzt seine KP um seine Attacke zu verstärken!",
"absorbedElectricity": "{{pokemonName}} absorbiert elektrische Energie!",
"switchedStatChanges": "{{pokemonName}} tauschte die Statuswerteveränderungen mit dem Ziel!",
"switchedTwoStatChanges": "{{pokemonName}} tauscht Veränderungen an {{firstStat}} und {{secondStat}} mit dem Ziel!",
"switchedStat": "{{pokemonName}} tauscht seinen {{stat}}-Wert mit dem des Zieles!",
"sharedGuard": "{{pokemonName}} addiert seine Schutzkräfte mit jenen des Zieles und teilt sie gerecht auf!",
"sharedPower": "{{pokemonName}} addiert seine Kräfte mit jenen des Zieles und teilt sie gerecht auf!",
"goingAllOutForAttack": "{{pokemonName}} legt sich ins Zeug!",
"regainedHealth": "{{pokemonName}} erholt sich!",
"keptGoingAndCrashed": "{{pokemonName}} springt daneben und verletzt sich!",

View File

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

View File

@ -97,9 +97,9 @@
"name": "Master League Champion",
"name_female": "Master League Champion"
},
"TRANSFER_MAX_BATTLE_STAT": {
"TRANSFER_MAX_STAT_STAGE": {
"name": "Teamwork",
"description": "Baton pass to another party member with at least one stat maxed out"
"description": "Baton pass to another party member with at least one stat stage maxed out"
},
"MAX_FRIENDSHIP": {
"name": "Friendmaxxing",

View File

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

View File

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

View File

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

View File

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

View File

@ -91,7 +91,7 @@
"name": "Campeón Liga Master",
"name_female": "Campeona Liga Master"
},
"TRANSFER_MAX_BATTLE_STAT": {
"TRANSFER_MAX_STAT_STAGE": {
"name": "Trabajo en Equipo",
"description": "Haz relevo a otro miembro del equipo con al menos una estadística al máximo."
},

View File

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

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!",
"burnedItselfOut": "¡El fuego interior de {{pokemonName}} se ha extinguido!",
"startedHeatingUpBeak": "¡{{pokemonName}} empieza\na calentar su pico!",

View File

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

View File

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

View File

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

View File

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

View File

@ -80,7 +80,7 @@
"100_RIBBONS": {
"name": "Campione Lega Assoluta"
},
"TRANSFER_MAX_BATTLE_STAT": {
"TRANSFER_MAX_STAT_STAGE": {
"name": "Lavoro di Squadra",
"description": "Trasferisci almeno sei bonus statistiche tramite staffetta"
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,10 @@
"cutHpPowerUpMove": "{{pokemonName}}[[는]]\n체력을 깎아서 자신의 기술을 강화했다!",
"absorbedElectricity": "{{pokemonName}}는(은)\n전기를 흡수했다!",
"switchedStatChanges": "{{pokemonName}}[[는]] 상대와 자신의\n능력 변화를 바꿨다!",
"switchedTwoStatChanges": "{{pokemonName}} 상대와 자신의 {{firstStat}}과 {{secondStat}}의 능력 변화를 바꿨다!",
"switchedStat": "{{pokemonName}} 서로의 {{stat}}를 교체했다!",
"sharedGuard": "{{pokemonName}} 서로의 가드를 셰어했다!",
"sharedPower": "{{pokemonName}} 서로의 파워를 셰어했다!",
"goingAllOutForAttack": "{{pokemonName}}[[는]]\n전력을 다하기 시작했다!",
"regainedHealth": "{{pokemonName}}[[는]]\n기력을 회복했다!",
"keptGoingAndCrashed": "{{pokemonName}}[[는]]\n의욕이 넘쳐서 땅에 부딪쳤다!",

View File

@ -84,7 +84,7 @@
"100_RIBBONS": {
"name": "Fita de Diamante"
},
"TRANSFER_MAX_BATTLE_STAT": {
"TRANSFER_MAX_STAT_STAGE": {
"name": "Trabalho em Equipe",
"description": "Use Baton Pass com pelo menos um atributo aumentado ao máximo"
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{
"Stat": {
"HP": "最大HP",
"HPshortened": "最大HP",
"HPshortened": "HP",
"ATK": "攻击",
"ATKshortened": "攻击",
"DEF": "防御",

View File

@ -80,7 +80,7 @@
"100_RIBBONS": {
"name": "大師球聯盟冠軍"
},
"TRANSFER_MAX_BATTLE_STAT": {
"TRANSFER_MAX_STAT_STAGE": {
"name": "團隊協作",
"description": "在一項屬性強化至最大時用接力棒傳遞給其他寶可夢"
},

View File

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

View File

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

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

View File

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

View File

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

View File

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

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);
orderedTargets.sort((a: Pokemon, b: Pokemon) => {
const aSpeed = a?.getBattleStat(Stat.SPD) || 0;
const bSpeed = b?.getBattleStat(Stat.SPD) || 0;
const aSpeed = a?.getEffectiveStat(Stat.SPD) || 0;
const bSpeed = b?.getEffectiveStat(Stat.SPD) || 0;
return bSpeed - aSpeed;
});

View File

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

View File

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

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

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

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

View File

@ -1,4 +1,3 @@
import { BattleStat } from "#app/data/battle-stat";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { StatusEffect } from "#app/enums/status-effect";
import Pokemon from "#app/field/pokemon";
@ -13,6 +12,7 @@ import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
import { Stat } from "#enums/stat";
describe("Abilities - Gulp Missile", () => {
let phaserGame: Phaser.Game;
@ -107,7 +107,7 @@ describe("Abilities - Gulp Missile", () => {
expect(cramorant.formIndex).toBe(GULPING_FORM);
});
it("deals ¼ of the attacker's maximum HP when hit by a damaging attack", async () => {
it("deals 1/4 of the attacker's maximum HP when hit by a damaging attack", async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
await game.startBattle([Species.CRAMORANT]);
@ -139,7 +139,7 @@ describe("Abilities - Gulp Missile", () => {
expect(cramorant.formIndex).toBe(GULPING_FORM);
});
it("lowers the attacker's Defense by 1 stage when hit in Gulping form", async () => {
it("lowers attacker's DEF stat stage by 1 when hit in Gulping form", async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
await game.startBattle([Species.CRAMORANT]);
@ -158,7 +158,7 @@ describe("Abilities - Gulp Missile", () => {
await game.phaseInterceptor.to(TurnEndPhase);
expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy));
expect(enemy.summonData.battleStats[BattleStat.DEF]).toBe(-1);
expect(enemy.getStatStage(Stat.DEF)).toBe(-1);
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeUndefined();
expect(cramorant.formIndex).toBe(NORMAL_FORM);
});
@ -219,7 +219,7 @@ describe("Abilities - Gulp Missile", () => {
await game.phaseInterceptor.to(TurnEndPhase);
expect(enemy.hp).toBe(enemyHpPreEffect);
expect(enemy.summonData.battleStats[BattleStat.DEF]).toBe(-1);
expect(enemy.getStatStage(Stat.DEF)).toBe(-1);
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeUndefined();
expect(cramorant.formIndex).toBe(NORMAL_FORM);
});

View File

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

View File

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

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

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

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

View File

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

View File

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

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

View File

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

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

View File

@ -1,6 +1,6 @@
import { BattlerIndex } from "#app/battle";
import { applyAbAttrs, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability";
import { Stat } from "#app/data/pokemon-stat";
import { Stat } from "#enums/stat";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import * as Utils from "#app/utils";
import { Abilities } from "#enums/abilities";

View File

@ -1,6 +1,6 @@
import { BattlerIndex } from "#app/battle";
import { applyAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr, PostDefendTypeChangeAbAttr } from "#app/data/ability";
import { Stat } from "#app/data/pokemon-stat";
import { Stat } from "#enums/stat";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import * as Utils from "#app/utils";
import { Abilities } from "#enums/abilities";

View File

@ -1,6 +1,6 @@
import { BattlerIndex } from "#app/battle";
import { applyAbAttrs, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability";
import { Stat } from "#app/data/pokemon-stat";
import { Stat } from "#enums/stat";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import * as Utils from "#app/utils";
import { Abilities } from "#enums/abilities";

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

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

View File

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

View File

@ -224,7 +224,7 @@ describe("achvs", () => {
expect(achvs._50_RIBBONS).toBeInstanceOf(RibbonAchv);
expect(achvs._75_RIBBONS).toBeInstanceOf(RibbonAchv);
expect(achvs._100_RIBBONS).toBeInstanceOf(RibbonAchv);
expect(achvs.TRANSFER_MAX_BATTLE_STAT).toBeInstanceOf(Achv);
expect(achvs.TRANSFER_MAX_STAT_STAGE).toBeInstanceOf(Achv);
expect(achvs.MAX_FRIENDSHIP).toBeInstanceOf(Achv);
expect(achvs.MEGA_EVOLVE).toBeInstanceOf(Achv);
expect(achvs.GIGANTAMAX).toBeInstanceOf(Achv);

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

View File

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

View File

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

View File

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

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

View File

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

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

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

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

View File

@ -1,13 +1,10 @@
import { BattlerIndex } from "#app/battle";
import { CritBoosterModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import * as Utils from "#app/utils";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phase from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
describe("Items - Scope Lens", () => {
let phaserGame: Phaser.Game;
@ -26,47 +23,29 @@ describe("Items - Scope Lens", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.enemySpecies(Species.MAGIKARP);
game.override.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
game.override.disableCrits();
game.override
.enemySpecies(Species.MAGIKARP)
.enemyMoveset(SPLASH_ONLY)
.moveset([ Moves.POUND ])
.startingHeldItems([{ name: "SCOPE_LENS" }])
.battleType("single")
.disableCrits();
game.override.battleType("single");
}, 20000);
it("SCOPE_LENS activates in battle correctly", async () => {
game.override.startingHeldItems([{ name: "SCOPE_LENS" }]);
game.override.moveset([Moves.POUND]);
const consoleSpy = vi.spyOn(console, "log");
it("should raise CRIT stage by 1", async () => {
await game.startBattle([
Species.GASTLY
]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "getCritStage");
game.move.select(Moves.POUND);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to(MoveEffectPhase);
expect(consoleSpy).toHaveBeenCalledWith("Applied", "Scope Lens", "");
}, 20000);
it("SCOPE_LENS held by random pokemon", async () => {
await game.startBattle([
Species.GASTLY
]);
const partyMember = game.scene.getPlayerPokemon()!;
// Making sure modifier is not applied without holding item
const critLevel = new Utils.IntegerHolder(0);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
expect(critLevel.value).toBe(0);
// Giving Scope Lens to party member and testing if it applies
partyMember.scene.addModifier(modifierTypes.SCOPE_LENS().newModifier(partyMember), true);
partyMember.scene.applyModifiers(CritBoosterModifier, true, partyMember, critLevel);
expect(critLevel.value).toBe(1);
expect(enemyPokemon.getCritStage).toHaveReturnedWith(1);
}, 20000);
});

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