Merge branch 'beta' into waveData
This commit is contained in:
commit
3876ba93be
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
export enum MoveEffectTrigger {
|
||||
PRE_APPLY,
|
||||
POST_APPLY,
|
||||
HIT,
|
||||
/** Triggers one time after all target effects have applied */
|
||||
POST_TARGET
|
||||
}
|
||||
|
|
|
@ -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];
|
|
@ -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
|
||||
);
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
@ -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)) {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue