Merge branch 'beta' into waveData

This commit is contained in:
Bertie690 2025-04-22 20:23:24 -04:00 committed by GitHub
commit 3876ba93be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 1117 additions and 1088 deletions

View File

@ -9,3 +9,8 @@ export const SESSION_ID_COOKIE_NAME: string = "pokerogue_sessionId";
/** Max value for an integer attribute in {@linkcode SystemSaveData} */
export const MAX_INT_ATTR_VALUE = 0x80000000;
/** The min and max waves for mystery encounters to spawn in classic mode */
export const CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [10, 180] as const;
/** The min and max waves for mystery encounters to spawn in challenge mode */
export const CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [10, 180] as const;

View File

@ -658,8 +658,8 @@ export class MoveImmunityStatStageChangeAbAttr extends MoveImmunityAbAttr {
*/
export class ReverseDrainAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return move.hasAttr(HitHealAttr) && !move.hitsSubstitute(attacker, pokemon);
override canApplyPostDefend(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _attacker: Pokemon, move: Move, _hitResult: HitResult | null, args: any[]): boolean {
return move.hasAttr(HitHealAttr);
}
/**
@ -698,7 +698,7 @@ export class PostDefendStatStageChangeAbAttr extends PostDefendAbAttr {
}
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return this.condition(pokemon, attacker, move) && !move.hitsSubstitute(attacker, pokemon);
return this.condition(pokemon, attacker, move);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
@ -739,7 +739,7 @@ export class PostDefendHpGatedStatStageChangeAbAttr extends PostDefendAbAttr {
const hpGateFlat: number = Math.ceil(pokemon.getMaxHp() * this.hpGate);
const lastAttackReceived = pokemon.turnData.attacksReceived[pokemon.turnData.attacksReceived.length - 1];
const damageReceived = lastAttackReceived?.damage || 0;
return this.condition(pokemon, attacker, move) && (pokemon.hp <= hpGateFlat && (pokemon.hp + damageReceived) > hpGateFlat) && !move.hitsSubstitute(attacker, pokemon);
return this.condition(pokemon, attacker, move) && (pokemon.hp <= hpGateFlat && (pokemon.hp + damageReceived) > hpGateFlat);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
@ -762,7 +762,7 @@ export class PostDefendApplyArenaTrapTagAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
const tag = globalScene.arena.getTag(this.tagType) as ArenaTrapTag;
return (this.condition(pokemon, attacker, move) && !move.hitsSubstitute(attacker, pokemon))
return (this.condition(pokemon, attacker, move))
&& (!globalScene.arena.getTag(this.tagType) || tag.layers < tag.maxLayers);
}
@ -784,7 +784,7 @@ export class PostDefendApplyBattlerTagAbAttr extends PostDefendAbAttr {
}
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return this.condition(pokemon, attacker, move) && !move.hitsSubstitute(attacker, pokemon);
return this.condition(pokemon, attacker, move);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
@ -801,7 +801,7 @@ export class PostDefendTypeChangeAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
this.type = attacker.getMoveType(move);
const pokemonTypes = pokemon.getTypes(true);
return hitResult < HitResult.NO_EFFECT && !move.hitsSubstitute(attacker, pokemon) && (simulated || pokemonTypes.length !== 1 || pokemonTypes[0] !== this.type);
return hitResult < HitResult.NO_EFFECT && (simulated || pokemonTypes.length !== 1 || pokemonTypes[0] !== this.type);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, _args: any[]): void {
@ -828,7 +828,7 @@ export class PostDefendTerrainChangeAbAttr extends PostDefendAbAttr {
}
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
return hitResult < HitResult.NO_EFFECT && !move.hitsSubstitute(attacker, pokemon) && globalScene.arena.canSetTerrain(this.terrainType);
return hitResult < HitResult.NO_EFFECT && globalScene.arena.canSetTerrain(this.terrainType);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, _args: any[]): void {
@ -852,7 +852,7 @@ export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randSeedInt(this.effects.length)];
return move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon}) && !attacker.status
&& (this.chance === -1 || pokemon.randSeedInt(100) < this.chance) && !move.hitsSubstitute(attacker, pokemon)
&& (this.chance === -1 || pokemon.randSeedInt(100) < this.chance)
&& attacker.canSetStatus(effect, true, false, pokemon);
}
@ -892,7 +892,7 @@ export class PostDefendContactApplyTagChanceAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon}) && pokemon.randSeedInt(100) < this.chance
&& !move.hitsSubstitute(attacker, pokemon) && attacker.canAddTag(this.tagType);
&& attacker.canAddTag(this.tagType);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
@ -913,10 +913,6 @@ export class PostDefendCritStatStageChangeAbAttr extends PostDefendAbAttr {
this.stages = stages;
}
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return !move.hitsSubstitute(attacker, pokemon);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
if (!simulated) {
globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), true, [ this.stat ], this.stages));
@ -939,7 +935,7 @@ export class PostDefendContactDamageAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return !simulated && move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon})
&& !attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr) && !move.hitsSubstitute(attacker, pokemon);
&& !attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
@ -998,7 +994,7 @@ export class PostDefendWeatherChangeAbAttr extends PostDefendAbAttr {
}
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return (!(this.condition && !this.condition(pokemon, attacker, move) || move.hitsSubstitute(attacker, pokemon))
return (!(this.condition && !this.condition(pokemon, attacker, move))
&& !globalScene.arena.weather?.isImmutable() && globalScene.arena.canSetWeather(this.weatherType));
}
@ -1016,7 +1012,7 @@ export class PostDefendAbilitySwapAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon})
&& attacker.getAbility().isSwappable && !move.hitsSubstitute(attacker, pokemon);
&& attacker.getAbility().isSwappable;
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, args: any[]): void {
@ -1042,10 +1038,10 @@ export class PostDefendAbilityGiveAbAttr extends PostDefendAbAttr {
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon}) && attacker.getAbility().isSuppressable
&& !attacker.getAbility().hasAttr(PostDefendAbilityGiveAbAttr) && !move.hitsSubstitute(attacker, pokemon);
&& !attacker.getAbility().hasAttr(PostDefendAbilityGiveAbAttr);
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
override applyPostDefend(_pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
if (!simulated) {
attacker.setTempAbility(allAbilities[this.ability]);
}
@ -1071,7 +1067,7 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr {
}
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
return attacker.getTag(BattlerTagType.DISABLED) === null && !move.hitsSubstitute(attacker, pokemon)
return attacker.getTag(BattlerTagType.DISABLED) === null
&& move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon}) && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance);
}
@ -1775,7 +1771,6 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr {
override canApplyPostAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
if (
super.canApplyPostAttack(pokemon, passive, simulated, attacker, move, hitResult, args)
&& !(pokemon !== attacker && move.hitsSubstitute(attacker, pokemon))
&& (simulated || !attacker.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && pokemon !== attacker
&& (!this.contactRequired || move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon})) && pokemon.randSeedInt(100) < this.chance && !pokemon.status)
) {
@ -1842,8 +1837,7 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr {
if (
!simulated &&
hitResult < HitResult.NO_EFFECT &&
(!this.condition || this.condition(pokemon, attacker, move)) &&
!move.hitsSubstitute(attacker, pokemon)
(!this.condition || this.condition(pokemon, attacker, move))
) {
const heldItems = this.getTargetHeldItems(attacker).filter((i) => i.isTransferable);
if (heldItems.length) {
@ -5160,6 +5154,8 @@ export class PostSummonStatStageChangeOnArenaAbAttr extends PostSummonStatStageC
/**
* Takes no damage from the first hit of a damaging move.
* This is used in the Disguise and Ice Face abilities.
*
* Does not apply to a user's substitute
* @extends ReceivedMoveDamageMultiplierAbAttr
*/
export class FormBlockDamageAbAttr extends ReceivedMoveDamageMultiplierAbAttr {
@ -7499,4 +7495,4 @@ export function initAbilities() {
.unreplaceable() // TODO is this true?
.attr(ConfusionOnStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)
);
}
}

View File

@ -7,7 +7,7 @@ import { MoveTarget } from "#enums/MoveTarget";
import { MoveCategory } from "#enums/MoveCategory";
import { getPokemonNameWithAffix } from "#app/messages";
import type Pokemon from "#app/field/pokemon";
import { HitResult, PokemonMove } from "#app/field/pokemon";
import { HitResult } from "#app/field/pokemon";
import { StatusEffect } from "#enums/status-effect";
import type { BattlerIndex } from "#app/battle";
import {
@ -335,7 +335,7 @@ export class ConditionalProtectTag extends ArenaTag {
* @param arena the {@linkcode Arena} containing this tag
* @param simulated `true` if the tag is applied quietly; `false` otherwise.
* @param isProtected a {@linkcode BooleanHolder} used to flag if the move is protected against
* @param attacker the attacking {@linkcode Pokemon}
* @param _attacker the attacking {@linkcode Pokemon}
* @param defender the defending {@linkcode Pokemon}
* @param moveId the {@linkcode Moves | identifier} for the move being used
* @param ignoresProtectBypass a {@linkcode BooleanHolder} used to flag if a protection effect supercedes effects that ignore protection
@ -345,7 +345,7 @@ export class ConditionalProtectTag extends ArenaTag {
arena: Arena,
simulated: boolean,
isProtected: BooleanHolder,
attacker: Pokemon,
_attacker: Pokemon,
defender: Pokemon,
moveId: Moves,
ignoresProtectBypass: BooleanHolder,
@ -354,8 +354,6 @@ export class ConditionalProtectTag extends ArenaTag {
if (!isProtected.value) {
isProtected.value = true;
if (!simulated) {
attacker.stopMultiHit(defender);
new CommonBattleAnim(CommonAnim.PROTECT, defender).play();
globalScene.queueMessage(
i18next.t("arenaTag:conditionalProtectApply", {
@ -899,7 +897,7 @@ export class DelayedAttackTag extends ArenaTag {
if (!ret) {
globalScene.unshiftPhase(
new MoveEffectPhase(this.sourceId!, [this.targetIndex], new PokemonMove(this.sourceMove!, 0, 0, true)),
new MoveEffectPhase(this.sourceId!, [this.targetIndex], allMoves[this.sourceMove!], false, true),
); // TODO: are those bangs correct?
}

View File

@ -2641,7 +2641,7 @@ export class GulpMissileTag extends BattlerTag {
return false;
}
if (moveEffectPhase.move.getMove().hitsSubstitute(attacker, pokemon)) {
if (moveEffectPhase.move.hitsSubstitute(attacker, pokemon)) {
return true;
}
@ -2997,7 +2997,7 @@ export class SubstituteTag extends BattlerTag {
if (!attacker) {
return;
}
const move = moveEffectPhase.move.getMove();
const move = moveEffectPhase.move;
const firstHit = attacker.turnData.hitCount === attacker.turnData.hitsLeft;
if (firstHit && move.hitsSubstitute(attacker, pokemon)) {
@ -3685,7 +3685,7 @@ function getMoveEffectPhaseData(_pokemon: Pokemon): { phase: MoveEffectPhase; at
return {
phase: phase,
attacker: phase.getPokemon(),
move: phase.move.getMove(),
move: phase.move,
};
}
return null;

View File

@ -0,0 +1,20 @@
import { MoveTarget } from "#enums/MoveTarget";
import type Move from "./move";
/**
* Return whether the move targets the field
*
* Examples include
* - Hazard moves like spikes
* - Weather moves like rain dance
* - User side moves like reflect and safeguard
*/
export function isFieldTargeted(move: Move): boolean {
switch (move.moveTarget) {
case MoveTarget.BOTH_SIDES:
case MoveTarget.USER_SIDE:
case MoveTarget.ENEMY_SIDE:
return true;
}
return false;
}

View File

@ -60,6 +60,7 @@ import {
MoveTypeChangeAbAttr,
PostDamageForceSwitchAbAttr,
PostItemLostAbAttr,
ReflectStatusMoveAbAttr,
ReverseDrainAbAttr,
UserFieldMoveTypePowerBoostAbAttr,
VariableMovePowerAbAttr,
@ -665,6 +666,17 @@ export default class Move implements Localizable {
return true;
}
break;
case MoveFlags.REFLECTABLE:
// If the target is not semi-invulnerable and either has magic coat active or an unignored magic bounce ability
if (
target?.getTag(SemiInvulnerableTag) ||
!(target?.getTag(BattlerTagType.MAGIC_COAT) ||
(!this.doesFlagEffectApply({ flag: MoveFlags.IGNORE_ABILITIES, user, target }) &&
target?.hasAbilityWithAttr(ReflectStatusMoveAbAttr)))
) {
return false;
}
break;
}
return !!(this.flags & flag);
@ -1716,7 +1728,7 @@ export class SacrificialAttr extends MoveEffectAttr {
**/
export class SacrificialAttrOnHit extends MoveEffectAttr {
constructor() {
super(true, { trigger: MoveEffectTrigger.HIT });
super(true);
}
/**
@ -1955,6 +1967,14 @@ export class PartyStatusCureAttr extends MoveEffectAttr {
* @extends MoveEffectAttr
*/
export class FlameBurstAttr extends MoveEffectAttr {
constructor() {
/**
* This is self-targeted to bypass immunity to target-facing secondary
* effects when the target has an active Substitute doll.
* TODO: Find a more intuitive way to implement Substitute bypassing.
*/
super(true);
}
/**
* @param user - n/a
* @param target - The target Pokémon.
@ -2177,7 +2197,7 @@ export class HitHealAttr extends MoveEffectAttr {
private healStat: EffectiveStat | null;
constructor(healRatio?: number | null, healStat?: EffectiveStat) {
super(true, { trigger: MoveEffectTrigger.HIT });
super(true);
this.healRatio = healRatio ?? 0.5;
this.healStat = healStat ?? null;
@ -2426,7 +2446,7 @@ export class StatusEffectAttr extends MoveEffectAttr {
public overrideStatus: boolean = false;
constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) {
super(selfTarget, { trigger: MoveEffectTrigger.HIT });
super(selfTarget);
this.effect = effect;
this.turnsRemaining = turnsRemaining;
@ -2434,10 +2454,6 @@ export class StatusEffectAttr extends MoveEffectAttr {
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!this.selfTarget && move.hitsSubstitute(user, target)) {
return false;
}
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
const statusCheck = moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance;
if (statusCheck) {
@ -2495,7 +2511,7 @@ export class MultiStatusEffectAttr extends StatusEffectAttr {
export class PsychoShiftEffectAttr extends MoveEffectAttr {
constructor() {
super(false, { trigger: MoveEffectTrigger.HIT });
super(false);
}
/**
@ -2534,15 +2550,11 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
private chance: number;
constructor(chance: number) {
super(false, { trigger: MoveEffectTrigger.HIT });
super(false);
this.chance = chance;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (move.hitsSubstitute(user, target)) {
return false;
}
const rand = Phaser.Math.RND.realInRange(0, 1);
if (rand >= this.chance) {
return false;
@ -2590,7 +2602,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
private berriesOnly: boolean;
constructor(berriesOnly: boolean) {
super(false, { trigger: MoveEffectTrigger.HIT });
super(false);
this.berriesOnly = berriesOnly;
}
@ -2600,17 +2612,13 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
* @param target Target {@linkcode Pokemon} that the moves applies to
* @param move {@linkcode Move} that is used
* @param args N/A
* @returns {boolean} True if an item was removed
* @returns True if an item was removed
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!this.berriesOnly && target.isPlayer()) { // "Wild Pokemon cannot knock off Player Pokemon's held items" (See Bulbapedia)
return false;
}
if (move.hitsSubstitute(user, target)) {
return false;
}
const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // Check for abilities that block item theft
@ -2664,8 +2672,9 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
*/
export class EatBerryAttr extends MoveEffectAttr {
protected chosenBerry: BerryModifier;
constructor() {
super(true, { trigger: MoveEffectTrigger.HIT });
protected chosenBerry: BerryModifier | undefined;
constructor(selfTarget: boolean) {
super(selfTarget);
}
/**
@ -2681,7 +2690,9 @@ export class EatBerryAttr extends MoveEffectAttr {
return false;
}
const heldBerries = this.getTargetHeldBerries(target);
const pokemon = this.selfTarget ? user : target;
const heldBerries = this.getTargetHeldBerries(pokemon);
if (heldBerries.length <= 0) {
// no berries makes munchlax very sad...
return false;
@ -2690,9 +2701,10 @@ export class EatBerryAttr extends MoveEffectAttr {
// pick a random berry to gobble and check if we preserve it
this.chosenBerry = heldBerries[user.randSeedInt(heldBerries.length)];
const preserve = new BooleanHolder(false);
globalScene.applyModifiers(PreserveBerryModifier, target.isPlayer(), target, preserve); // check for berry pouch preservation
// check for berry pouch preservation
globalScene.applyModifiers(PreserveBerryModifier, pokemon.isPlayer(), pokemon, preserve);
if (!preserve.value) {
this.reduceBerryModifier(target);
this.reduceBerryModifier(pokemon);
}
// Don't update harvest for berries preserved via Berry pouch (no item dupes lol)
@ -2737,14 +2749,14 @@ export class EatBerryAttr extends MoveEffectAttr {
*/
export class StealEatBerryAttr extends EatBerryAttr {
constructor() {
super();
super(false);
}
/**
* User steals a random berry from the target and then eats it.
* @param user the {@linkcode Pokemon} using the move; will eat the stolen berry
* @param target the {@linkcode Pokemon} having its berry stolen
* @param move the {@linkcode Move} being used
* @param user - The {@linkcode Pokemon} using the move; will eat the stolen berry
* @param target - The {@linkcode Pokemon} having its berry stolen
* @param move - The {@linkcode Move} being used
* @param args N/A
* @returns `true` if the function succeeds
*/
@ -2762,7 +2774,7 @@ export class StealEatBerryAttr extends EatBerryAttr {
}
// check if the target even _has_ a berry in the first place
// TODO: Check if Pluck displays messages when used against sticky hold mons w/o berries
// TODO: Check on cart if Pluck displays messages when used against sticky hold mons w/o berries
const heldBerries = this.getTargetHeldBerries(target);
if (heldBerries.length <= 0) {
return false;
@ -2809,10 +2821,6 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
return false;
}
if (!this.selfTarget && move.hitsSubstitute(user, target)) {
return false;
}
// Special edge case for shield dust blocking Sparkling Aria curing burn
const moveTargets = getMoveTargets(user, move.id);
if (target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && move.id === Moves.SPARKLING_ARIA && moveTargets.targets.length === 1) {
@ -3189,15 +3197,7 @@ export class StatStageChangeAttr extends MoveEffectAttr {
private get showMessage () {
return this.options?.showMessage ?? true;
}
/**
* Indicates when the stat change should trigger
* @default MoveEffectTrigger.HIT
*/
public override get trigger () {
return this.options?.trigger ?? MoveEffectTrigger.HIT;
}
/**
* Attempts to change stats of the user or target (depending on value of selfTarget) if conditions are met
* @param user {@linkcode Pokemon} the user of the move
@ -3211,10 +3211,6 @@ export class StatStageChangeAttr extends MoveEffectAttr {
return false;
}
if (!this.selfTarget && move.hitsSubstitute(user, target)) {
return false;
}
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) {
const stages = this.getLevels(user);
@ -3498,7 +3494,7 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
*/
export class OrderUpStatBoostAttr extends MoveEffectAttr {
constructor() {
super(true, { trigger: MoveEffectTrigger.HIT });
super(true);
}
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
@ -3575,17 +3571,15 @@ export class ResetStatsAttr extends MoveEffectAttr {
this.targetAllPokemon = targetAllPokemon;
}
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
override apply(_user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
if (this.targetAllPokemon) {
// Target all pokemon on the field when Freezy Frost or Haze are used
const activePokemon = globalScene.getField(true);
activePokemon.forEach((p) => this.resetStats(p));
globalScene.queueMessage(i18next.t("moveTriggers:statEliminated"));
} else { // Affects only the single target when Clear Smog is used
if (!move.hitsSubstitute(user, target)) {
this.resetStats(target);
globalScene.queueMessage(i18next.t("moveTriggers:resetStats", { pokemonName: getPokemonNameWithAffix(target) }));
}
this.resetStats(target);
globalScene.queueMessage(i18next.t("moveTriggers:resetStats", { pokemonName: getPokemonNameWithAffix(target) }));
}
return true;
}
@ -4237,7 +4231,8 @@ export class PresentPowerAttr extends VariablePowerAttr {
(args[0] as NumberHolder).value = 120;
} else if (80 < powerSeed && powerSeed <= 100) {
// If this move is multi-hit, disable all other hits
user.stopMultiHit();
user.turnData.hitCount = 1;
user.turnData.hitsLeft = 1;
globalScene.unshiftPhase(new PokemonHealPhase(target.getBattlerIndex(),
toDmgValue(target.getMaxHp() / 4), i18next.t("moveTriggers:regainedHealth", { pokemonName: getPokemonNameWithAffix(target) }), true));
}
@ -4831,8 +4826,8 @@ export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const category = (args[0] as NumberHolder);
const predictedPhysDmg = target.getBaseDamage(user, move, MoveCategory.PHYSICAL, true, true, true, true);
const predictedSpecDmg = target.getBaseDamage(user, move, MoveCategory.SPECIAL, true, true, true, true);
const predictedPhysDmg = target.getBaseDamage({source: user, move, moveCategory: MoveCategory.PHYSICAL, ignoreAbility: true, ignoreSourceAbility: true, ignoreAllyAbility: true, ignoreSourceAllyAbility: true, simulated: true});
const predictedSpecDmg = target.getBaseDamage({source: user, move, moveCategory: MoveCategory.SPECIAL, ignoreAbility: true, ignoreSourceAbility: true, ignoreAllyAbility: true, ignoreSourceAllyAbility: true, simulated: true});
if (predictedPhysDmg > predictedSpecDmg) {
category.value = MoveCategory.PHYSICAL;
@ -5391,7 +5386,7 @@ export class BypassRedirectAttr extends MoveAttr {
export class FrenzyAttr extends MoveEffectAttr {
constructor() {
super(true, { trigger: MoveEffectTrigger.HIT, lastHitOnly: true });
super(true, { lastHitOnly: true });
}
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) {
@ -5463,22 +5458,20 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
protected cancelOnFail: boolean;
private failOnOverlap: boolean;
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: number = 0, turnCountMax?: number, lastHitOnly: boolean = false, cancelOnFail: boolean = false) {
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: number = 0, turnCountMax?: number, lastHitOnly: boolean = false) {
super(selfTarget, { lastHitOnly: lastHitOnly });
this.tagType = tagType;
this.turnCountMin = turnCountMin;
this.turnCountMax = turnCountMax !== undefined ? turnCountMax : turnCountMin;
this.failOnOverlap = !!failOnOverlap;
this.cancelOnFail = cancelOnFail;
}
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.canApply(user, target, move, args) || (this.cancelOnFail === true && user.getLastXMoves(1)[0]?.result === MoveResult.FAIL)) {
if (!super.canApply(user, target, move, args)) {
return false;
} else {
return true;
}
return true;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -5569,19 +5562,6 @@ export class LeechSeedAttr extends AddBattlerTagAttr {
constructor() {
super(BattlerTagType.SEEDED);
}
/**
* Adds a Seeding effect to the target if the target does not have an active Substitute.
* @param user the {@linkcode Pokemon} using the move
* @param target the {@linkcode Pokemon} targeted by the move
* @param move the {@linkcode Move} invoking this effect
* @param args n/a
* @returns `true` if the effect successfully applies; `false` otherwise
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return !move.hitsSubstitute(user, target)
&& super.apply(user, target, move, args);
}
}
/**
@ -5757,13 +5737,6 @@ export class FlinchAttr extends AddBattlerTagAttr {
constructor() {
super(BattlerTagType.FLINCHED, false);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!move.hitsSubstitute(user, target)) {
return super.apply(user, target, move, args);
}
return false;
}
}
export class ConfuseAttr extends AddBattlerTagAttr {
@ -5779,16 +5752,13 @@ export class ConfuseAttr extends AddBattlerTagAttr {
return false;
}
if (!move.hitsSubstitute(user, target)) {
return super.apply(user, target, move, args);
}
return false;
return super.apply(user, target, move, args);
}
}
export class RechargeAttr extends AddBattlerTagAttr {
constructor() {
super(BattlerTagType.RECHARGING, true, false, 1, 1, true, true);
super(BattlerTagType.RECHARGING, true, false, 1, 1, true);
}
}
@ -6171,7 +6141,7 @@ export class AddPledgeEffectAttr extends AddArenaTagAttr {
* @see {@linkcode apply}
*/
export class RevivalBlessingAttr extends MoveEffectAttr {
constructor(user?: boolean) {
constructor() {
super(true);
}
@ -6412,10 +6382,6 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
const player = switchOutTarget instanceof PlayerPokemon;
if (!this.selfSwitch) {
if (move.hitsSubstitute(user, target)) {
return false;
}
// Dondozo with an allied Tatsugiri in its mouth cannot be forced out
const commandedTag = switchOutTarget.getTag(BattlerTagType.COMMANDED);
if (commandedTag?.getSourcePokemon()?.isActive(true)) {
@ -6670,7 +6636,7 @@ export class ChangeTypeAttr extends MoveEffectAttr {
private type: PokemonType;
constructor(type: PokemonType) {
super(false, { trigger: MoveEffectTrigger.HIT });
super(false);
this.type = type;
}
@ -6693,7 +6659,7 @@ export class AddTypeAttr extends MoveEffectAttr {
private type: PokemonType;
constructor(type: PokemonType) {
super(false, { trigger: MoveEffectTrigger.HIT });
super(false);
this.type = type;
}
@ -7389,7 +7355,7 @@ export class AbilityChangeAttr extends MoveEffectAttr {
public ability: Abilities;
constructor(ability: Abilities, selfTarget?: boolean) {
super(selfTarget, { trigger: MoveEffectTrigger.HIT });
super(selfTarget);
this.ability = ability;
}
@ -7420,7 +7386,7 @@ export class AbilityCopyAttr extends MoveEffectAttr {
public copyToPartner: boolean;
constructor(copyToPartner: boolean = false) {
super(false, { trigger: MoveEffectTrigger.HIT });
super(false);
this.copyToPartner = copyToPartner;
}
@ -7461,7 +7427,7 @@ export class AbilityGiveAttr extends MoveEffectAttr {
public copyToPartner: boolean;
constructor() {
super(false, { trigger: MoveEffectTrigger.HIT });
super(false);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -7740,7 +7706,7 @@ export class DiscourageFrequentUseAttr extends MoveAttr {
export class MoneyAttr extends MoveEffectAttr {
constructor() {
super(true, { trigger: MoveEffectTrigger.HIT, firstHitOnly: true });
super(true, {firstHitOnly: true });
}
apply(user: Pokemon, target: Pokemon, move: Move): boolean {
@ -7807,7 +7773,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr {
public effect: StatusEffect;
constructor(effect: StatusEffect) {
super(true, { trigger: MoveEffectTrigger.HIT });
super(true);
this.effect = effect;
}
@ -10591,7 +10557,7 @@ export function initMoves() {
.attr(JawLockAttr)
.bitingMove(),
new SelfStatusMove(Moves.STUFF_CHEEKS, PokemonType.NORMAL, -1, 10, -1, 0, 8)
.attr(EatBerryAttr)
.attr(EatBerryAttr, true)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true)
.condition((user) => {
const userBerries = globalScene.findModifiers(m => m instanceof BerryModifier, user.isPlayer());
@ -10615,7 +10581,7 @@ export function initMoves() {
.makesContact(false)
.partial(), // smart targetting is unimplemented
new StatusMove(Moves.TEATIME, PokemonType.NORMAL, -1, 10, -1, 0, 8)
.attr(EatBerryAttr)
.attr(EatBerryAttr, false)
.target(MoveTarget.ALL),
new StatusMove(Moves.OCTOLOCK, PokemonType.FIGHTING, 100, 15, -1, 0, 8)
.condition(failIfGhostTypeCondition)

View File

@ -22,7 +22,7 @@ import { EggTier } from "#enums/egg-type";
import { PartyHealPhase } from "#app/phases/party-heal-phase";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { modifierTypes } from "#app/modifier/modifier-type";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/aTrainersTest";

View File

@ -37,7 +37,7 @@ import type HeldModifierConfig from "#app/interfaces/held-modifier-config";
import type { BerryType } from "#enums/berry-type";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import i18next from "i18next";
/** the i18n namespace for this encounter */

View File

@ -23,7 +23,7 @@ import { speciesStarterCosts } from "#app/data/balance/starters";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import i18next from "i18next";
/** the i18n namespace for this encounter */

View File

@ -36,7 +36,7 @@ import i18next from "#app/plugins/i18n";
import { BerryType } from "#enums/berry-type";
import { PERMANENT_STATS, Stat } from "#enums/stat";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/berriesAbound";

View File

@ -52,7 +52,7 @@ import i18next from "i18next";
import MoveInfoOverlay from "#app/ui/move-info-overlay";
import { allMoves } from "#app/data/moves/move";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
/** the i18n namespace for the encounter */

View File

@ -46,7 +46,7 @@ import { Moves } from "#enums/moves";
import { EncounterBattleAnim } from "#app/data/battle-anims";
import { MoveCategory } from "#enums/MoveCategory";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { EncounterAnim } from "#enums/encounter-anims";
import { Challenges } from "#enums/challenges";

View File

@ -24,7 +24,7 @@ import { TrainerSlot } from "#enums/trainer-slot";
import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
import { EnemyPokemon, PokemonMove } from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { modifierTypes } from "#app/modifier/modifier-type";
import { LearnMovePhase } from "#app/phases/learn-move-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";

View File

@ -19,7 +19,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
import type { PokemonHeldItemModifier } from "#app/modifier/modifier";
import { PokemonFormChangeItemModifier } from "#app/modifier/modifier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { Challenges } from "#enums/challenges";
/** i18n namespace for encounter */

View File

@ -18,7 +18,7 @@ import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/u
import { getPokemonSpecies } from "#app/data/pokemon-species";
import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import type { PokemonHeldItemModifier, PokemonInstantReviveModifier } from "#app/modifier/modifier";
import {
BerryModifier,

View File

@ -10,7 +10,7 @@ import { Species } from "#enums/species";
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** i18n namespace for encounter */
const namespace = "mysteryEncounters/departmentStoreSale";

View File

@ -18,7 +18,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { Stat } from "#enums/stat";
import i18next from "i18next";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** i18n namespace for the encounter */
const namespace = "mysteryEncounters/fieldTrip";

View File

@ -41,7 +41,7 @@ import {
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { EncounterAnim } from "#enums/encounter-anims";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";

View File

@ -33,7 +33,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { randSeedInt } from "#app/utils/common";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/fightOrFlight";

View File

@ -30,7 +30,7 @@ import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
import { PostSummonPhase } from "#app/phases/post-summon-phase";
import { modifierTypes } from "#app/modifier/modifier-type";
import { Nature } from "#enums/nature";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
/** the i18n namespace for the encounter */

View File

@ -48,7 +48,7 @@ import { Gender, getGenderSymbol } from "#app/data/gender";
import { getNatureName } from "#app/data/nature";
import { getPokeballAtlasKey, getPokeballTintColor } from "#app/data/pokeball";
import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import type { PokeballType } from "#enums/pokeball";
import { doShinySparkleAnim } from "#app/field/anims";

View File

@ -10,7 +10,7 @@ import { leaveEncounterWithoutBattle, setEncounterExp } from "../utils/encounter
import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { PokemonMove } from "#app/field/pokemon";
const OPTION_1_REQUIRED_MOVE = Moves.SURF;

View File

@ -16,7 +16,7 @@ import { randSeedInt } from "#app/utils/common";
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/mysteriousChallengers";

View File

@ -15,7 +15,7 @@ import {
koPlayerPokemon,
} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { GameOverPhase } from "#app/phases/game-over-phase";
import { randSeedInt } from "#app/utils/common";

View File

@ -20,7 +20,7 @@ import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-enco
import i18next from "i18next";
import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
/** the i18n namespace for the encounter */

View File

@ -31,7 +31,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { ScanIvsPhase } from "#app/phases/scan-ivs-phase";
import { SummonPhase } from "#app/phases/summon-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { NON_LEGEND_PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
/** the i18n namespace for the encounter */

View File

@ -26,7 +26,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import type { Nature } from "#enums/nature";
import { getNatureName } from "#app/data/nature";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import i18next from "i18next";
/** the i18n namespace for this encounter */

View File

@ -26,7 +26,7 @@ import { getPokemonSpecies } from "#app/data/pokemon-species";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { PartyHealPhase } from "#app/phases/party-heal-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { BerryType } from "#enums/berry-type";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";

View File

@ -29,7 +29,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { getPokemonNameWithAffix } from "#app/messages";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import {
getEncounterPokemonLevelForWave,
STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER,

View File

@ -11,7 +11,7 @@ import { randSeedShuffle } from "#app/utils/common";
import type MysteryEncounter from "../mystery-encounter";
import { MysteryEncounterBuilder } from "../mystery-encounter";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { Biome } from "#enums/biome";
import { TrainerType } from "#enums/trainer-type";
import i18next from "i18next";

View File

@ -3,7 +3,7 @@ import {
transitionMysteryEncounterIntroVisuals,
updatePlayerMoney,
} from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { isNullOrUndefined, randSeedInt } from "#app/utils/common";
import { isNullOrUndefined, NumberHolder, randSeedInt, randSeedItem } from "#app/utils/common";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { globalScene } from "#app/global-scene";
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
@ -26,9 +26,10 @@ import { showEncounterDialogue } from "#app/data/mystery-encounters/utils/encoun
import PokemonData from "#app/system/pokemon-data";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { Abilities } from "#enums/abilities";
import { NON_LEGEND_PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
import { NON_LEGEND_PARADOX_POKEMON, NON_LEGEND_ULTRA_BEASTS } from "#app/data/balance/special-species-groups";
import { timedEventManager } from "#app/global-event-manager";
/** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/thePokemonSalesman";
@ -38,6 +39,9 @@ const MAX_POKEMON_PRICE_MULTIPLIER = 4;
/** Odds of shiny magikarp will be 1/value */
const SHINY_MAGIKARP_WEIGHT = 100;
/** Odds of event sale will be value/100 */
const EVENT_THRESHOLD = 50;
/**
* Pokemon Salesman encounter.
* @see {@link https://github.com/pagefaultgames/pokerogue/issues/3799 | GitHub Issue #3799}
@ -82,15 +86,46 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
tries++;
}
const r = randSeedInt(SHINY_MAGIKARP_WEIGHT);
const validEventEncounters = timedEventManager
.getEventEncounters()
.filter(
s =>
!getPokemonSpecies(s.species).legendary &&
!getPokemonSpecies(s.species).subLegendary &&
!getPokemonSpecies(s.species).mythical &&
!NON_LEGEND_PARADOX_POKEMON.includes(s.species) &&
!NON_LEGEND_ULTRA_BEASTS.includes(s.species),
);
let pokemon: PlayerPokemon;
/**
* Mon is determined as follows:
* If you roll the 1% for Shiny Magikarp, you get Magikarp with a random variant
* If an event with more than 1 valid event encounter species is active, you have 20% chance to get one of those
* If the rolled species has no HA, and there are valid event encounters, you will get one of those
* If the rolled species has no HA and there are no valid event encounters, you will get Shiny Magikarp
* Mons rolled from the event encounter pool get 2 extra shiny rolls
*/
if (
randSeedInt(SHINY_MAGIKARP_WEIGHT) === 0 ||
isNullOrUndefined(species.abilityHidden) ||
species.abilityHidden === Abilities.NONE
r === 0 ||
((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) &&
(validEventEncounters.length === 0))
) {
// If no HA mon found or you roll 1%, give shiny Magikarp with random variant
// If you roll 1%, give shiny Magikarp with random variant
species = getPokemonSpecies(Species.MAGIKARP);
pokemon = new PlayerPokemon(species, 5, 2, species.formIndex, undefined, true);
pokemon = new PlayerPokemon(species, 5, 2, undefined, undefined, true);
} else if (
(validEventEncounters.length > 0 && (r <= EVENT_THRESHOLD ||
(isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE)))
) {
// If you roll 20%, give event encounter with 2 extra shiny rolls and its HA, if it has one
const enc = randSeedItem(validEventEncounters);
species = getPokemonSpecies(enc.species);
pokemon = new PlayerPokemon(species, 5, species.abilityHidden === Abilities.NONE ? undefined : 2, enc.formIndex);
pokemon.trySetShinySeed();
pokemon.trySetShinySeed();
} else {
pokemon = new PlayerPokemon(species, 5, 2, species.formIndex);
}

View File

@ -28,7 +28,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { Stat } from "#enums/stat";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/theStrongStuff";

View File

@ -32,7 +32,7 @@ import { ShowTrainerPhase } from "#app/phases/show-trainer-phase";
import { ReturnPhase } from "#app/phases/return-phase";
import i18next from "i18next";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { BattlerTagType } from "#enums/battler-tag-type";
/** the i18n namespace for the encounter */

View File

@ -28,7 +28,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
import type HeldModifierConfig from "#app/interfaces/held-modifier-config";
import i18next from "i18next";
import { getStatKey } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import type { Nature } from "#enums/nature";

View File

@ -26,7 +26,7 @@ import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Moves } from "#enums/moves";
import { BattlerIndex } from "#app/battle";
import { PokemonMove } from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { randSeedInt } from "#app/utils/common";
/** the i18n namespace for this encounter */

View File

@ -37,7 +37,7 @@ import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encoun
import { BerryModifier } from "#app/modifier/modifier";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/uncommonBreed";

View File

@ -1067,8 +1067,8 @@ export function getRandomEncounterSpecies(level: number, isBoss = false, rerollH
ret.formIndex = formIndex;
}
//Reroll shiny for event encounters
if (isEventEncounter && !ret.shiny) {
//Reroll shiny or variant for event encounters
if (isEventEncounter) {
ret.trySetShinySeed();
}
//Reroll hidden ability

View File

@ -1,7 +1,6 @@
export enum MoveEffectTrigger {
PRE_APPLY,
POST_APPLY,
HIT,
/** Triggers one time after all target effects have applied */
POST_TARGET
}

View File

@ -0,0 +1,23 @@
/** The result of a hit check calculation */
export const HitCheckResult = {
/** Hit checks haven't been evaluated yet in this pass */
PENDING: 0,
/** The move hits the target successfully */
HIT: 1,
/** The move has no effect on the target */
NO_EFFECT: 2,
/** The move has no effect on the target, but doesn't proc the default "no effect" message */
NO_EFFECT_NO_MESSAGE: 3,
/** The target protected itself against the move */
PROTECTED: 4,
/** The move missed the target */
MISS: 5,
/** The move is reflected by magic coat or magic bounce */
REFLECTED: 6,
/** The target is no longer on the field */
TARGET_NOT_ON_FIELD: 7,
/** The move failed unexpectedly */
ERROR: 8,
} as const;
export type HitCheckResult = typeof HitCheckResult[keyof typeof HitCheckResult];

View File

@ -278,6 +278,36 @@ export enum FieldPosition {
RIGHT,
}
/** Base typeclass for damage parameter methods, used for DRY */
type damageParams = {
/** The attacking {@linkcode Pokemon} */
source: Pokemon;
/** The move used in the attack */
move: Move;
/** The move's {@linkcode MoveCategory} after variable-category effects are applied */
moveCategory: MoveCategory;
/** If `true`, ignores this Pokemon's defensive ability effects */
ignoreAbility?: boolean;
/** If `true`, ignores the attacking Pokemon's ability effects */
ignoreSourceAbility?: boolean;
/** If `true`, ignores the ally Pokemon's ability effects */
ignoreAllyAbility?: boolean;
/** If `true`, ignores the ability effects of the attacking pokemon's ally */
ignoreSourceAllyAbility?: boolean;
/** If `true`, calculates damage for a critical hit */
isCritical?: boolean;
/** If `true`, suppresses changes to game state during the calculation */
simulated?: boolean;
/** If defined, used in place of calculated effectiveness values */
effectiveness?: number;
}
/** Type for the parameters of {@linkcode Pokemon#getBaseDamage | getBaseDamage} */
type getBaseDamageParams = Omit<damageParams, "effectiveness">
/** Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage} */
type getAttackDamageParams = Omit<damageParams, "moveCategory">;
export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* This pokemon's {@link https://bulbapedia.bulbagarden.net/wiki/Personality_value | Personality value/PID},
@ -1463,26 +1493,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Calculate the critical-hit stage of a move used against this pokemon by
* the given source
*
* @param source the {@linkcode Pokemon} who using the move
* @param move the {@linkcode Move} being used
* @returns the final critical-hit stage value
* @param source - The {@linkcode Pokemon} 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 NumberHolder(0);
applyMoveAttrs(HighCritAttr, source, this, move, critStage);
globalScene.applyModifiers(
CritBoosterModifier,
source.isPlayer(),
source,
critStage,
);
globalScene.applyModifiers(
TempCritBoosterModifier,
source.isPlayer(),
critStage,
);
applyAbAttrs(BonusCritAbAttr, source, null, false, critStage)
globalScene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage);
globalScene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage);
applyAbAttrs(BonusCritAbAttr, source, null, false, critStage);
const critBoostTag = source.getTag(CritBoostTag);
if (critBoostTag) {
if (critBoostTag instanceof DragonCheerTag) {
@ -1498,6 +1518,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return critStage.value;
}
/**
* Calculates the category of a move when used by this pokemon after
* category-changing move effects are applied.
* @param target - The {@linkcode Pokemon} using the move
* @param move - The {@linkcode Move} being used
* @returns The given move's final category
*/
getMoveCategory(target: Pokemon, move: Move): MoveCategory {
const moveCategory = new NumberHolder(move.category);
applyMoveAttrs(VariableMoveCategoryAttr, this, target, move, moveCategory);
return moveCategory.value;
}
/**
* Calculates and retrieves the final value of a stat considering any held
* items, move effects, opponent abilities, and whether there was a critical
@ -2604,7 +2637,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param simulated Whether to apply abilities via simulated calls (defaults to `true`)
* @param cancelled {@linkcode BooleanHolder} Stores whether the move was cancelled by a non-type-based immunity.
* @param useIllusion - Whether we want the attack move effectiveness on the illusion or not
* Currently only used by {@linkcode Pokemon.apply} to determine whether a "No effect" message should be shown.
* @returns The type damage multiplier, indicating the effectiveness of the move
*/
getMoveEffectiveness(
@ -3190,7 +3222,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Function that tries to set a Pokemon shiny based on seed.
* For manual use only, usually to roll a Pokemon's shiny chance a second time.
* If it rolls shiny, also sets a random variant and give the Pokemon the associated luck.
* If it rolls shiny, or if it's already shiny, also sets a random variant and give the Pokemon the associated luck.
*
* The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536`
* @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
@ -3201,29 +3233,31 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
thresholdOverride?: number,
applyModifiersToOverride?: boolean,
): boolean {
const shinyThreshold = new NumberHolder(BASE_SHINY_CHANCE);
if (thresholdOverride === undefined || applyModifiersToOverride) {
if (thresholdOverride !== undefined && applyModifiersToOverride) {
shinyThreshold.value = thresholdOverride;
}
if (timedEventManager.isEventActive()) {
shinyThreshold.value *= timedEventManager.getShinyMultiplier();
}
if (!this.hasTrainer()) {
if (!this.shiny) {
const shinyThreshold = new NumberHolder(BASE_SHINY_CHANCE);
if (thresholdOverride === undefined || applyModifiersToOverride) {
if (thresholdOverride !== undefined && applyModifiersToOverride) {
shinyThreshold.value = thresholdOverride;
}
if (timedEventManager.isEventActive()) {
shinyThreshold.value *= timedEventManager.getShinyMultiplier();
}
globalScene.applyModifiers(
ShinyRateBoosterModifier,
true,
shinyThreshold,
);
}
} else {
shinyThreshold.value = thresholdOverride;
else {
shinyThreshold.value = thresholdOverride;
}
this.shiny = randSeedInt(65536) < shinyThreshold.value;
}
this.shiny = randSeedInt(65536) < shinyThreshold.value;
if (this.shiny) {
this.variant = this.generateShinyVariant();
this.variant = this.variant ?? 0;
this.variant = Math.max(this.generateShinyVariant(), this.variant) as Variant; // Don't set a variant lower than the current one
this.luck =
this.variant + 1 + (this.fusionShiny ? this.fusionVariant + 1 : 0);
this.initShinySparkle();
@ -4093,27 +4127,28 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Calculates the base damage of the given move against this Pokemon when attacked by the given source.
* Used during damage calculation and for Shell Side Arm's forecasting effect.
* @param source the attacking {@linkcode Pokemon}.
* @param move the {@linkcode Move} used in the attack.
* @param moveCategory the move's {@linkcode MoveCategory} after variable-category effects are applied.
* @param ignoreAbility if `true`, ignores this Pokemon's defensive ability effects (defaults to `false`).
* @param ignoreSourceAbility if `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`).
* @param ignoreAllyAbility if `true`, ignores the ally Pokemon's ability effects (defaults to `false`).
* @param ignoreSourceAllyAbility if `true`, ignores the attacking Pokemon's ally's ability effects (defaults to `false`).
* @param isCritical if `true`, calculates effective stats as if the hit were critical (defaults to `false`).
* @param simulated if `true`, suppresses changes to game state during calculation (defaults to `true`).
* @param source - The attacking {@linkcode Pokemon}.
* @param move - The {@linkcode Move} used in the attack.
* @param moveCategory - The move's {@linkcode MoveCategory} after variable-category effects are applied.
* @param ignoreAbility - If `true`, ignores this Pokemon's defensive ability effects (defaults to `false`).
* @param ignoreSourceAbility - If `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`).
* @param ignoreAllyAbility - If `true`, ignores the ally Pokemon's ability effects (defaults to `false`).
* @param ignoreSourceAllyAbility - If `true`, ignores the attacking Pokemon's ally's ability effects (defaults to `false`).
* @param isCritical - if `true`, calculates effective stats as if the hit were critical (defaults to `false`).
* @param simulated - if `true`, suppresses changes to game state during calculation (defaults to `true`).
* @returns The move's base damage against this Pokemon when used by the source Pokemon.
*/
getBaseDamage(
source: Pokemon,
move: Move,
moveCategory: MoveCategory,
{
source,
move,
moveCategory,
ignoreAbility = false,
ignoreSourceAbility = false,
ignoreAllyAbility = false,
ignoreSourceAllyAbility = false,
isCritical = false,
simulated = true,
simulated = true}: getBaseDamageParams
): number {
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
@ -4240,27 +4275,27 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Calculates the damage of an attack made by another Pokemon against this Pokemon
* @param source {@linkcode Pokemon} the attacking Pokemon
* @param move {@linkcode Pokemon} the move used in the attack
* @param move The {@linkcode Move} used in the attack
* @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects
* @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects
* @param ignoreAllyAbility If `true`, ignores the ally Pokemon's ability effects
* @param ignoreSourceAllyAbility If `true`, ignores the ability effects of the attacking pokemon's ally
* @param isCritical If `true`, calculates damage for a critical hit.
* @param simulated If `true`, suppresses changes to game state during the calculation.
* @returns a {@linkcode DamageCalculationResult} object with three fields:
* - `cancelled`: `true` if the move was cancelled by another effect.
* - `result`: {@linkcode HitResult} indicates the attack's type effectiveness.
* - `damage`: `number` the attack's final damage output.
* @param effectiveness If defined, used in place of calculated effectiveness values
* @returns The {@linkcode DamageCalculationResult}
*/
getAttackDamage(
source: Pokemon,
move: Move,
ignoreAbility = false,
ignoreSourceAbility = false,
ignoreAllyAbility = false,
ignoreSourceAllyAbility = false,
isCritical = false,
simulated = true,
{
source,
move,
ignoreAbility = false,
ignoreSourceAbility = false,
ignoreAllyAbility = false,
ignoreSourceAllyAbility = false,
isCritical = false,
simulated = true,
effectiveness}: getAttackDamageParams,
): DamageCalculationResult {
const damage = new NumberHolder(0);
const defendingSide = this.isPlayer()
@ -4290,7 +4325,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
*
* Note that the source's abilities are not ignored here
*/
const typeMultiplier = this.getMoveEffectiveness(
const typeMultiplier = effectiveness ?? this.getMoveEffectiveness(
source,
move,
ignoreAbility,
@ -4362,7 +4397,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* The attack's base damage, as determined by the source's level, move power
* and Attack stat as well as this Pokemon's Defense stat
*/
const baseDamage = this.getBaseDamage(
const baseDamage = this.getBaseDamage({
source,
move,
moveCategory,
@ -4372,7 +4407,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
ignoreSourceAllyAbility,
isCritical,
simulated,
);
});
/** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */
const { targets, multiple } = getMoveTargets(source, move.id);
@ -4583,211 +4618,36 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
};
}
/**
* Applies the results of a move to this pokemon
* @param source The {@linkcode Pokemon} using the move
* @param move The {@linkcode Move} being used
* @returns The {@linkcode HitResult} of the attack
*/
apply(source: Pokemon, move: Move): HitResult {
const defendingSide = this.isPlayer()
? ArenaTagSide.PLAYER
: ArenaTagSide.ENEMY;
const moveCategory = new NumberHolder(move.category);
applyMoveAttrs(VariableMoveCategoryAttr, source, this, move, moveCategory);
if (moveCategory.value === MoveCategory.STATUS) {
const cancelled = new BooleanHolder(false);
const typeMultiplier = this.getMoveEffectiveness(
source,
move,
false,
false,
cancelled,
);
if (!cancelled.value && typeMultiplier === 0) {
globalScene.queueMessage(
i18next.t("battle:hitResultNoEffect", {
pokemonName: getPokemonNameWithAffix(this),
}),
);
}
return typeMultiplier === 0 ? HitResult.NO_EFFECT : HitResult.STATUS;
/** Calculate whether the given move critically hits this pokemon
* @param source - The {@linkcode Pokemon} using the move
* @param move - The {@linkcode Move} being used
* @param simulated - If `true`, suppresses changes to game state during calculation (defaults to `true`)
* @returns whether the move critically hits the pokemon
*/
getCriticalHitResult(source: Pokemon, move: Move, simulated: boolean = true): boolean {
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
const noCritTag = globalScene.arena.getTagOnSide(NoCritTag, defendingSide);
if (noCritTag || Overrides.NEVER_CRIT_OVERRIDE || move.hasAttr(FixedDamageAttr)) {
return false;
}
/** Determines whether the attack critically hits */
let isCritical: boolean;
const critOnly = new BooleanHolder(false);
const critAlways = source.getTag(BattlerTagType.ALWAYS_CRIT);
applyMoveAttrs(CritOnlyAttr, source, this, move, critOnly);
applyAbAttrs(
ConditionalCritAbAttr,
source,
null,
false,
critOnly,
this,
move,
);
if (critOnly.value || critAlways) {
isCritical = true;
} else {
const isCritical = new BooleanHolder(false);
if (source.getTag(BattlerTagType.ALWAYS_CRIT)) {
isCritical.value = true;
}
applyMoveAttrs(CritOnlyAttr, source, this, move, isCritical);
applyAbAttrs(ConditionalCritAbAttr, source, null, simulated, isCritical, this, move);
if (!isCritical.value) {
const critChance = [24, 8, 2, 1][
Math.max(0, Math.min(this.getCritStage(source, move), 3))
];
isCritical =
critChance === 1 || !globalScene.randBattleSeedInt(critChance);
isCritical.value = critChance === 1 || !globalScene.randBattleSeedInt(critChance);
}
const noCritTag = globalScene.arena.getTagOnSide(NoCritTag, defendingSide);
const blockCrit = new BooleanHolder(false);
applyAbAttrs(BlockCritAbAttr, this, null, false, blockCrit);
if (noCritTag || blockCrit.value || Overrides.NEVER_CRIT_OVERRIDE) {
isCritical = false;
}
applyAbAttrs(BlockCritAbAttr, this, null, simulated, isCritical);
/**
* Applies stat changes from {@linkcode move} and gives it to {@linkcode source}
* before damage calculation
*/
applyMoveAttrs(StatChangeBeforeDmgCalcAttr, source, this, move);
const {
cancelled,
result,
damage: dmg,
} = this.getAttackDamage(source, move, false, false, false, false, isCritical, false);
const typeBoost = source.findTag(
t =>
t instanceof TypeBoostTag && t.boostedType === source.getMoveType(move),
) as TypeBoostTag;
if (typeBoost?.oneUse) {
source.removeTag(typeBoost.tagType);
}
if (
cancelled ||
result === HitResult.IMMUNE ||
result === HitResult.NO_EFFECT
) {
source.stopMultiHit(this);
if (!cancelled) {
if (result === HitResult.IMMUNE) {
globalScene.queueMessage(
i18next.t("battle:hitResultImmune", {
pokemonName: getPokemonNameWithAffix(this),
}),
);
} else {
globalScene.queueMessage(
i18next.t("battle:hitResultNoEffect", {
pokemonName: getPokemonNameWithAffix(this),
}),
);
}
}
return result;
}
// In case of fatal damage, this tag would have gotten cleared before we could lapse it.
const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND);
const grudgeTag = this.getTag(BattlerTagType.GRUDGE);
if (dmg) {
this.lapseTags(BattlerTagLapseType.HIT);
const substitute = this.getTag(SubstituteTag);
const isBlockedBySubstitute =
!!substitute && move.hitsSubstitute(source, this);
if (isBlockedBySubstitute) {
substitute.hp -= dmg;
}
if (!this.isPlayer() && dmg >= this.hp) {
globalScene.applyModifiers(EnemyEndureChanceModifier, false, this);
}
/**
* We explicitly require to ignore the faint phase here, as we want to show the messages
* about the critical hit and the super effective/not very effective messages before the faint phase.
*/
const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg,
{
result: result as DamageResult,
isCritical,
ignoreFaintPhase: true,
source
});
if (damage > 0) {
if (source.isPlayer()) {
globalScene.validateAchvs(DamageAchv, new NumberHolder(damage));
if (damage > globalScene.gameData.gameStats.highestDamage) {
globalScene.gameData.gameStats.highestDamage = damage;
}
}
source.turnData.totalDamageDealt += damage;
source.turnData.singleHitDamageDealt = damage;
this.turnData.damageTaken += damage;
this.battleData.hitCount++;
const attackResult = {
move: move.id,
result: result as DamageResult,
damage: damage,
critical: isCritical,
sourceId: source.id,
sourceBattlerIndex: source.getBattlerIndex(),
};
this.turnData.attacksReceived.unshift(attackResult);
if (source.isPlayer() && !this.isPlayer()) {
globalScene.applyModifiers(
DamageMoneyRewardModifier,
true,
source,
new NumberHolder(damage),
);
}
}
}
if (isCritical) {
globalScene.queueMessage(i18next.t("battle:hitResultCriticalHit"));
}
// want to include is.Fainted() in case multi hit move ends early, still want to render message
if (source.turnData.hitsLeft === 1 || this.isFainted()) {
switch (result) {
case HitResult.SUPER_EFFECTIVE:
globalScene.queueMessage(i18next.t("battle:hitResultSuperEffective"));
break;
case HitResult.NOT_VERY_EFFECTIVE:
globalScene.queueMessage(
i18next.t("battle:hitResultNotVeryEffective"),
);
break;
case HitResult.ONE_HIT_KO:
globalScene.queueMessage(i18next.t("battle:hitResultOneHitKO"));
break;
}
}
if (this.isFainted()) {
// set splice index here, so future scene queues happen before FaintedPhase
globalScene.setPhaseQueueSplice();
globalScene.unshiftPhase(
new FaintPhase(
this.getBattlerIndex(),
false,
source,
),
);
this.destroySubstitute();
this.lapseTag(BattlerTagType.COMMANDED);
}
return result;
return isCritical.value;
}
/**
@ -4852,7 +4712,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
/**
* Called by apply(), given the damage, adds a new DamagePhase and actually updates HP values, etc.
* Given the damage, adds a new DamagePhase and update HP values, etc.
*
* Checks for 'Indirect' HitResults to account for Endure/Reviver Seed applying correctly
* @param damage integer - passed to damage()
* @param result an enum if it's super effective, not very, etc.
@ -5156,8 +5017,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Gets whether the given move is currently disabled for this Pokemon.
*
* @param {Moves} moveId {@linkcode Moves} ID of the move to check
* @returns {boolean} `true` if the move is disabled for this Pokemon, otherwise `false`
* @param moveId - The {@linkcode Moves} ID of the move to check
* @returns `true` if the move is disabled for this Pokemon, otherwise `false`
*
* @see {@linkcode MoveRestrictionBattlerTag}
*/
@ -5168,9 +5029,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Gets whether the given move is currently disabled for the user based on the player's target selection
*
* @param {Moves} moveId {@linkcode Moves} ID of the move to check
* @param {Pokemon} user {@linkcode Pokemon} the move user
* @param {Pokemon} target {@linkcode Pokemon} the target of the move
* @param moveId - The {@linkcode Moves} ID of the move to check
* @param user - The move user
* @param target - The target of the move
*
* @returns {boolean} `true` if the move is disabled for this Pokemon due to the player's target selection
*
@ -5200,10 +5061,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Gets the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists.
*
* @param {Moves} moveId {@linkcode Moves} ID of the move to check
* @param {Pokemon} user {@linkcode Pokemon} the move user, optional and used when the target is a factor in the move's restricted status
* @param {Pokemon} target {@linkcode Pokemon} the target of the move, optional and used when the target is a factor in the move's restricted status
* @returns {MoveRestrictionBattlerTag | null} the first tag on this Pokemon that restricts the move, or `null` if the move is not restricted.
* @param moveId - {@linkcode Moves} ID of the move to check
* @param user - {@linkcode Pokemon} the move user, optional and used when the target is a factor in the move's restricted status
* @param target - {@linkcode Pokemon} the target of the move, optional and used when the target is a factor in the move's restricted status
* @returns The first tag on this Pokemon that restricts the move, or `null` if the move is not restricted.
*/
getRestrictingTag(
moveId: Moves,
@ -5265,20 +5126,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return this.summonData.moveQueue;
}
/**
* If this Pokemon is using a multi-hit move, cancels all subsequent strikes
* @param {Pokemon} target If specified, this only cancels subsequent strikes against the given target
*/
stopMultiHit(target?: Pokemon): void {
const effectPhase = globalScene.getCurrentPhase();
if (
effectPhase instanceof MoveEffectPhase &&
effectPhase.getUserPokemon() === this
) {
effectPhase.stopMultiHit(target);
}
}
changeForm(formChange: SpeciesFormChange): Promise<void> {
return new Promise(resolve => {
this.formIndex = Math.max(
@ -5692,7 +5539,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* cancel the attack's subsequent hits.
*/
if (effect === StatusEffect.SLEEP || effect === StatusEffect.FREEZE) {
this.stopMultiHit();
const currentPhase = globalScene.getCurrentPhase();
if (currentPhase instanceof MoveEffectPhase && currentPhase.getUserPokemon() === this) {
this.turnData.hitCount = 1;
this.turnData.hitsLeft = 1;
}
}
if (asPhase) {
@ -7336,14 +7187,15 @@ export class EnemyPokemon extends Pokemon {
].includes(move.id);
return (
doesNotFail &&
p.getAttackDamage(
this,
p.getAttackDamage({
source: this,
move,
!p.waveData.abilityRevealed,
false,
!p.getAlly()?.waveData.abilityRevealed,
false,
ignoreAbility: !p.waveData.abilityRevealed,
ignoreSourceAbility: false,
ignoreAllyAbility: !p.getAlly()?.waveData.abilityRevealed,
ignoreSourceAllyAbility: false,
isCritical,
}
).damage >= p.hp
);
})

View File

@ -13,6 +13,7 @@ import { Species } from "#enums/species";
import { Challenges } from "./enums/challenges";
import { globalScene } from "#app/global-scene";
import { getDailyStartingBiome } from "./data/daily-run";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES, CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES } from "./constants";
export enum GameModes {
CLASSIC,
@ -36,10 +37,6 @@ interface GameModeConfig {
hasMysteryEncounters?: boolean;
}
// Describes min and max waves for MEs in specific game modes
export const CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [10, 180];
export const CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [10, 180];
export class GameMode implements GameModeConfig {
public modeId: GameModes;
public isClassic: boolean;

View File

@ -35,19 +35,19 @@ import { BattlerTagType } from "#enums/battler-tag-type";
export class FaintPhase extends PokemonPhase {
/**
* Whether or not enduring (for this phase's purposes, Reviver Seed) should be prevented
* Whether or not instant revive should be prevented
*/
private preventEndure: boolean;
private preventInstantRevive: boolean;
/**
* The source Pokemon that dealt fatal damage
*/
private source?: Pokemon;
constructor(battlerIndex: BattlerIndex, preventEndure = false, source?: Pokemon) {
constructor(battlerIndex: BattlerIndex, preventInstantRevive = false, source?: Pokemon) {
super(battlerIndex);
this.preventEndure = preventEndure;
this.preventInstantRevive = preventInstantRevive;
this.source = source;
}
@ -63,7 +63,7 @@ export class FaintPhase extends PokemonPhase {
faintPokemon.resetSummonData();
if (!this.preventEndure) {
if (!this.preventInstantRevive) {
const instantReviveModifier = globalScene.applyModifier(
PokemonInstantReviveModifier,
this.player,

File diff suppressed because it is too large Load Diff

View File

@ -404,9 +404,10 @@ export class MovePhase extends BattlePhase {
* if the move fails.
*/
if (success) {
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
const move = this.move.getMove();
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, move);
globalScene.unshiftPhase(
new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, this.move, this.reflected),
new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, move, this.reflected, this.move.virtual),
);
} else {
if ([Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE].includes(this.move.moveId)) {

View File

@ -50,7 +50,11 @@ describe("Moves - Friend Guard", () => {
// Get the last return value from `getAttackDamage`
const turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
// Making sure the test is controlled; turn 1 damage is equal to base damage (after rounding)
expect(turn1Damage).toBe(Math.floor(player1.getBaseDamage(enemy1, allMoves[Moves.TACKLE], MoveCategory.PHYSICAL)));
expect(turn1Damage).toBe(
Math.floor(
player1.getBaseDamage({ source: enemy1, move: allMoves[Moves.TACKLE], moveCategory: MoveCategory.PHYSICAL }),
),
);
vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]);
@ -64,7 +68,10 @@ describe("Moves - Friend Guard", () => {
const turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
// With the ally's Friend Guard, damage should have been reduced from base damage by 25%
expect(turn2Damage).toBe(
Math.floor(player1.getBaseDamage(enemy1, allMoves[Moves.TACKLE], MoveCategory.PHYSICAL) * 0.75),
Math.floor(
player1.getBaseDamage({ source: enemy1, move: allMoves[Moves.TACKLE], moveCategory: MoveCategory.PHYSICAL }) *
0.75,
),
);
});

View File

@ -4,7 +4,6 @@ import { PokemonType } from "#enums/pokemon-type";
import { Abilities } from "#app/enums/abilities";
import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species";
import { HitResult } from "#app/field/pokemon";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@ -38,13 +37,13 @@ describe("Abilities - Galvanize", () => {
});
it("should change Normal-type attacks to Electric type and boost their power", async () => {
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "getMoveType");
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply");
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
const move = allMoves[Moves.TACKLE];
vi.spyOn(move, "calculateBattlePower");
@ -54,21 +53,23 @@ describe("Abilities - Galvanize", () => {
await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.getMoveType).toHaveLastReturnedWith(PokemonType.ELECTRIC);
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.EFFECTIVE);
expect(spy).toHaveReturnedWith(1);
expect(move.calculateBattlePower).toHaveReturnedWith(48);
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
spy.mockRestore();
});
it("should cause Normal-type attacks to activate Volt Absorb", async () => {
game.override.enemyAbility(Abilities.VOLT_ABSORB);
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "getMoveType");
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply");
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
enemyPokemon.hp = Math.floor(enemyPokemon.getMaxHp() * 0.8);
@ -77,37 +78,37 @@ describe("Abilities - Galvanize", () => {
await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.getMoveType).toHaveLastReturnedWith(PokemonType.ELECTRIC);
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.NO_EFFECT);
expect(spy).toHaveReturnedWith(0);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
});
it("should not change the type of variable-type moves", async () => {
game.override.enemySpecies(Species.MIGHTYENA);
await game.startBattle([Species.ESPEON]);
await game.classicMode.startBattle([Species.ESPEON]);
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "getMoveType");
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply");
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
game.move.select(Moves.REVELATION_DANCE);
await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.getMoveType).not.toHaveLastReturnedWith(PokemonType.ELECTRIC);
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.NO_EFFECT);
expect(spy).toHaveReturnedWith(0);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
});
it("should affect all hits of a Normal-type multi-hit move", async () => {
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "getMoveType");
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply");
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
game.move.select(Moves.FURY_SWIPES);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
@ -125,6 +126,6 @@ describe("Abilities - Galvanize", () => {
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
}
expect(enemyPokemon.apply).not.toHaveReturnedWith(HitResult.NO_EFFECT);
expect(spy).not.toHaveReturnedWith(0);
});
});

View File

@ -61,11 +61,11 @@ describe("Abilities - Infiltrator", () => {
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
const preScreenDmg = enemy.getAttackDamage(player, allMoves[move]).damage;
const preScreenDmg = enemy.getAttackDamage({ source: player, move: allMoves[move] }).damage;
game.scene.arena.addTag(tagType, 1, Moves.NONE, enemy.id, ArenaTagSide.ENEMY, true);
const postScreenDmg = enemy.getAttackDamage(player, allMoves[move]).damage;
const postScreenDmg = enemy.getAttackDamage({ source: player, move: allMoves[move] }).damage;
expect(postScreenDmg).toBe(preScreenDmg);
expect(player.waveData.abilitiesApplied).toContain(Abilities.INFILTRATOR);

View File

@ -4,6 +4,7 @@ import { MoveEndPhase } from "#app/phases/move-end-phase";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { HitCheckResult } from "#enums/hit-check-result";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest";
@ -28,6 +29,7 @@ describe("Abilities - No Guard", () => {
.moveset(Moves.ZAP_CANNON)
.ability(Abilities.NO_GUARD)
.enemyLevel(200)
.enemySpecies(Species.SNORLAX)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
@ -48,7 +50,7 @@ describe("Abilities - No Guard", () => {
await game.phaseInterceptor.to(MoveEndPhase);
expect(moveEffectPhase.hitCheck).toHaveReturnedWith(true);
expect(moveEffectPhase.hitCheck).toHaveReturnedWith([HitCheckResult.HIT, 1]);
});
it("should guarantee double battle with any one LURE", async () => {

View File

@ -52,7 +52,7 @@ describe("Abilities - Shield Dust", () => {
// Shield Dust negates secondary effect
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
const move = phase.move.getMove();
const move = phase.move;
expect(move.id).toBe(Moves.AIR_SLASH);
const chance = new NumberHolder(move.chance);

View File

@ -25,7 +25,6 @@ describe("Abilities - Super Luck", () => {
.moveset([Moves.TACKLE])
.ability(Abilities.SUPER_LUCK)
.battleStyle("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);

View File

@ -2,7 +2,6 @@ import { BattlerIndex } from "#app/battle";
import { Abilities } from "#app/enums/abilities";
import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species";
import { HitResult } from "#app/field/pokemon";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@ -87,13 +86,15 @@ describe("Abilities - Tera Shell", () => {
await game.classicMode.startBattle([Species.CHARIZARD]);
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "apply");
const spy = vi.spyOn(playerPokemon, "getMoveEffectiveness");
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.apply).toHaveLastReturnedWith(HitResult.EFFECTIVE);
expect(spy).toHaveLastReturnedWith(1);
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp() - 40);
spy.mockRestore();
});
it("should change the effectiveness of all strikes of a multi-strike move", async () => {
@ -102,7 +103,7 @@ describe("Abilities - Tera Shell", () => {
await game.classicMode.startBattle([Species.SNORLAX]);
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "apply");
const spy = vi.spyOn(playerPokemon, "getMoveEffectiveness");
game.move.select(Moves.SPLASH);
@ -110,8 +111,9 @@ describe("Abilities - Tera Shell", () => {
await game.move.forceHit();
for (let i = 0; i < 2; i++) {
await game.phaseInterceptor.to("MoveEffectPhase");
expect(playerPokemon.apply).toHaveLastReturnedWith(HitResult.NOT_VERY_EFFECTIVE);
expect(spy).toHaveLastReturnedWith(0.5);
}
expect(playerPokemon.apply).toHaveReturnedTimes(2);
expect(spy).toHaveReturnedTimes(2);
spy.mockRestore();
});
});

View File

@ -47,7 +47,9 @@ describe("Battle Mechanics - Damage Calculation", () => {
// expected base damage = [(2*level/5 + 2) * power * playerATK / enemyDEF / 50] + 2
// = 31.8666...
expect(enemyPokemon.getAttackDamage(playerPokemon, allMoves[Moves.TACKLE]).damage).toBeCloseTo(31);
expect(enemyPokemon.getAttackDamage({ source: playerPokemon, move: allMoves[Moves.TACKLE] }).damage).toBeCloseTo(
31,
);
});
it("Attacks deal 1 damage at minimum", async () => {
@ -91,7 +93,7 @@ describe("Battle Mechanics - Damage Calculation", () => {
const magikarp = game.scene.getPlayerPokemon()!;
const dragonite = game.scene.getEnemyPokemon()!;
expect(dragonite.getAttackDamage(magikarp, allMoves[Moves.DRAGON_RAGE]).damage).toBe(40);
expect(dragonite.getAttackDamage({ source: magikarp, move: allMoves[Moves.DRAGON_RAGE] }).damage).toBe(40);
});
it("One-hit KO moves ignore damage multipliers", async () => {
@ -102,7 +104,7 @@ describe("Battle Mechanics - Damage Calculation", () => {
const magikarp = game.scene.getPlayerPokemon()!;
const aggron = game.scene.getEnemyPokemon()!;
expect(aggron.getAttackDamage(magikarp, allMoves[Moves.FISSURE]).damage).toBe(aggron.hp);
expect(aggron.getAttackDamage({ source: magikarp, move: allMoves[Moves.FISSURE] }).damage).toBe(aggron.hp);
});
it("When the user fails to use Jump Kick with Wonder Guard ability, the damage should be 1.", async () => {

View File

@ -1,5 +1,5 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { PokemonTurnData, TurnMove, PokemonMove } from "#app/field/pokemon";
import type { PokemonTurnData, TurnMove } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
import { MoveResult } from "#app/field/pokemon";
import type BattleScene from "#app/battle-scene";
@ -185,12 +185,8 @@ describe("BattlerTag - SubstituteTag", () => {
vi.spyOn(mockPokemon.scene as BattleScene, "triggerPokemonBattleAnim").mockReturnValue(true);
vi.spyOn(mockPokemon.scene as BattleScene, "queueMessage").mockReturnValue();
const pokemonMove = {
getMove: vi.fn().mockReturnValue(allMoves[Moves.TACKLE]) as PokemonMove["getMove"],
} as PokemonMove;
const moveEffectPhase = {
move: pokemonMove,
move: allMoves[Moves.TACKLE],
getUserPokemon: vi.fn().mockReturnValue(undefined) as MoveEffectPhase["getUserPokemon"],
} as MoveEffectPhase;

View File

@ -36,8 +36,7 @@ describe("Items - Dire Hit", () => {
.enemyMoveset(Moves.SPLASH)
.moveset([Moves.POUND])
.startingHeldItems([{ name: "DIRE_HIT" }])
.battleStyle("single")
.disableCrits();
.battleStyle("single");
}, 20000);
it("should raise CRIT stage by 1", async () => {

View File

@ -28,7 +28,6 @@ describe("Items - Leek", () => {
.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH])
.startingHeldItems([{ name: "LEEK" }])
.moveset([Moves.TACKLE])
.disableCrits()
.battleStyle("single");
});

View File

@ -27,8 +27,7 @@ describe("Items - Scope Lens", () => {
.enemyMoveset(Moves.SPLASH)
.moveset([Moves.POUND])
.startingHeldItems([{ name: "SCOPE_LENS" }])
.battleStyle("single")
.disableCrits();
.battleStyle("single");
}, 20000);
it("should raise CRIT stage by 1", async () => {

View File

@ -97,14 +97,20 @@ describe("Moves - Dig", () => {
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
const preDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage;
const preDigEarthquakeDmg = playerPokemon.getAttackDamage({
source: enemyPokemon,
move: allMoves[Moves.EARTHQUAKE],
}).damage;
game.move.select(Moves.DIG);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEffectPhase");
const postDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage;
const postDigEarthquakeDmg = playerPokemon.getAttackDamage({
source: enemyPokemon,
move: allMoves[Moves.EARTHQUAKE],
}).damage;
// these hopefully get avoid rounding errors :shrug:
expect(postDigEarthquakeDmg).toBeGreaterThanOrEqual(2 * preDigEarthquakeDmg);
expect(postDigEarthquakeDmg).toBeLessThan(2 * (preDigEarthquakeDmg + 1));

View File

@ -50,7 +50,7 @@ describe("Moves - Dynamax Cannon", () => {
game.move.select(dynamaxCannon.id);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(dynamaxCannon.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(dynamaxCannon.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(100);
}, 20000);
@ -62,7 +62,7 @@ describe("Moves - Dynamax Cannon", () => {
game.move.select(dynamaxCannon.id);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(dynamaxCannon.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(dynamaxCannon.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(100);
}, 20000);
@ -75,7 +75,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id);
expect(phase.move.id).toBe(dynamaxCannon.id);
// Force level cap to be 100
vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamageAnimPhase, false);
@ -90,7 +90,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id);
expect(phase.move.id).toBe(dynamaxCannon.id);
// Force level cap to be 100
vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamageAnimPhase, false);
@ -105,7 +105,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id);
expect(phase.move.id).toBe(dynamaxCannon.id);
// Force level cap to be 100
vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamageAnimPhase, false);
@ -120,7 +120,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id);
expect(phase.move.id).toBe(dynamaxCannon.id);
// Force level cap to be 100
vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamageAnimPhase, false);
@ -135,7 +135,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id);
expect(phase.move.id).toBe(dynamaxCannon.id);
// Force level cap to be 100
vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamageAnimPhase, false);
@ -150,7 +150,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(dynamaxCannon.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(dynamaxCannon.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000);

View File

@ -57,12 +57,12 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000);
@ -77,12 +77,12 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000);
@ -97,7 +97,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(100);
@ -107,7 +107,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.phaseInterceptor.runFrom(MovePhase).to(MoveEndPhase);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000);
@ -123,7 +123,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(100);
@ -132,7 +132,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.phaseInterceptor.runFrom(MovePhase).to(MoveEndPhase);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100);
}, 20000);
@ -147,12 +147,12 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000);
@ -191,22 +191,22 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000);
@ -245,22 +245,22 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionBolt.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200);
await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(fusionFlare.id);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id);
await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000);

View File

@ -71,7 +71,7 @@ describe("Moves - Spectral Thief", () => {
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
const moveToCheck = allMoves[Moves.SPECTRAL_THIEF];
const dmgBefore = enemy.getAttackDamage(player, moveToCheck, false, false, false, false).damage;
const dmgBefore = enemy.getAttackDamage({ source: player, move: moveToCheck }).damage;
enemy.setStatStage(Stat.ATK, 6);
@ -80,7 +80,7 @@ describe("Moves - Spectral Thief", () => {
game.move.select(Moves.SPECTRAL_THIEF);
await game.phaseInterceptor.to(TurnEndPhase);
expect(dmgBefore).toBeLessThan(enemy.getAttackDamage(player, moveToCheck, false, false, false, false).damage);
expect(dmgBefore).toBeLessThan(enemy.getAttackDamage({ source: player, move: moveToCheck }).damage);
});
it("should steal stat stages as a negative value with Contrary.", async () => {

View File

@ -4,7 +4,6 @@ import { allMoves, TeraMoveCategoryAttr } from "#app/data/moves/move";
import type Move from "#app/data/moves/move";
import { PokemonType } from "#enums/pokemon-type";
import { Abilities } from "#app/enums/abilities";
import { HitResult } from "#app/field/pokemon";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/testUtils/gameManager";
@ -49,9 +48,9 @@ describe("Moves - Tera Blast", () => {
it("changes type to match user's tera type", async () => {
game.override.enemySpecies(Species.FURRET);
await game.startBattle();
await game.classicMode.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply");
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.teraType = PokemonType.FIGHTING;
@ -61,11 +60,11 @@ describe("Moves - Tera Blast", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE);
expect(spy).toHaveReturnedWith(2);
}, 20000);
it("increases power if user is Stellar tera type", async () => {
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.teraType = PokemonType.STELLAR;
@ -79,25 +78,25 @@ describe("Moves - Tera Blast", () => {
}, 20000);
it("is super effective against terastallized targets if user is Stellar tera type", async () => {
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.teraType = PokemonType.STELLAR;
playerPokemon.isTerastallized = true;
const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply");
const spy = vi.spyOn(enemyPokemon, "getMoveEffectiveness");
enemyPokemon.isTerastallized = true;
game.move.select(Moves.TERA_BLAST);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE);
expect(spy).toHaveReturnedWith(2);
});
it("uses the higher ATK for damage calculation", async () => {
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.stats[Stat.ATK] = 100;
@ -112,7 +111,7 @@ describe("Moves - Tera Blast", () => {
});
it("uses the higher SPATK for damage calculation", async () => {
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.stats[Stat.ATK] = 1;
@ -127,7 +126,7 @@ describe("Moves - Tera Blast", () => {
it("should stay as a special move if ATK turns lower than SPATK mid-turn", async () => {
game.override.enemyMoveset([Moves.CHARM]);
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.stats[Stat.ATK] = 51;
@ -145,7 +144,7 @@ describe("Moves - Tera Blast", () => {
game.override
.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "THICK_CLUB" }])
.starterSpecies(Species.CUBONE);
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
@ -163,7 +162,7 @@ describe("Moves - Tera Blast", () => {
it("does not change its move category from stat changes due to abilities", async () => {
game.override.ability(Abilities.HUGE_POWER);
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.stats[Stat.ATK] = 50;
@ -178,7 +177,7 @@ describe("Moves - Tera Blast", () => {
});
it("causes stat drops if user is Stellar tera type", async () => {
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.teraType = PokemonType.STELLAR;

View File

@ -18,29 +18,29 @@ import { vi } from "vitest";
*/
export class MoveHelper extends GameManagerHelper {
/**
* Intercepts {@linkcode MoveEffectPhase} and mocks the
* {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `true`.
* Used to force a move to hit.
* Intercepts {@linkcode MoveEffectPhase} and mocks the phase's move's
* accuracy to -1, guaranteeing a hit.
*/
public async forceHit(): Promise<void> {
await this.game.phaseInterceptor.to(MoveEffectPhase, false);
vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
const moveEffectPhase = this.game.scene.getCurrentPhase() as MoveEffectPhase;
vi.spyOn(moveEffectPhase.move, "calculateBattleAccuracy").mockReturnValue(-1);
}
/**
* Intercepts {@linkcode MoveEffectPhase} and mocks the
* {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `false`.
* Used to force a move to miss.
* Intercepts {@linkcode MoveEffectPhase} and mocks the phase's move's accuracy
* to 0, guaranteeing a miss.
* @param firstTargetOnly - Whether the move should force miss on the first target only, in the case of multi-target moves.
*/
public async forceMiss(firstTargetOnly = false): Promise<void> {
await this.game.phaseInterceptor.to(MoveEffectPhase, false);
const hitCheck = vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck");
const moveEffectPhase = this.game.scene.getCurrentPhase() as MoveEffectPhase;
const accuracy = vi.spyOn(moveEffectPhase.move, "calculateBattleAccuracy");
if (firstTargetOnly) {
hitCheck.mockReturnValueOnce(false);
accuracy.mockReturnValueOnce(0);
} else {
hitCheck.mockReturnValue(false);
accuracy.mockReturnValue(0);
}
}